子序列问题
子序列问题分类:
- 只涉及一个子串。
- 当前状态只和上一个状态有关,即 dp[i] 只和 dp[i-1] 状态有关。(53题)
- 当前状态和前面所有状态有关,即 dp[i] 和 dp[0-(i-1)] 状态有关。(300题)
-
53-最大子序列
题意分析: 判断子数组连续元素和最大。
解题思路:
- 动态规划。dp[i] 状态的结果一定是以 nums[i] 为结束元素的值。
注意点:
拓展:
- 如何输出和最大的连续子数组?
代码:
public int maxSubArray(int[] nums) {
int n = nums.length;
// 状态数组。nums[1...i] 以 nums[i] 结尾的子数组的最大和
int[] dp = new int[n];
// base case
dp[0] = nums[0];
// 状态转换
for (int i = 1; i < n; i++) {
dp[i] = Math.max(nums[i], dp[i-1] + nums[i]);
}
// 求解
int max_sum = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
if (dp[i] > max_sum) {
max_sum = dp[i];
}
}
return max_sum;
}
300-最长递增子序列(动规/动规+二分/贪心+二分)
题意分析:
- 从数组中找出最长递增的一串数字。
解题思路:
- 动态规划。当前状态 dp[i] 依赖前面的所有状态。
- 动规+二分 / 贪心 + 二分。 https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-by-leetcode-soluti/
注意点:
拓展:
- 如何输出最长递增子序列?
代码:
public int lengthOfLIS(int[] nums) {
int n = nums.length;
// 状态数组
int[] dp = new int[n];
// base case
Arrays.fill(dp, 1);
// 状态转换
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
// i 会不断更新,dp[i] 会存在覆盖的情况
dp[i] = Math.max(dp[j] + 1, dp[i]);
}
}
}
// System.out.println(Arrays.toString(dp));
// 求数组最大值等价于:ans = Arrays.stream(dp).max().getAsInt();
int ans = 0;
for (int i = 0; i < n; i++) {
ans = Math.max(ans, dp[i]);
}
return ans;
}
股票买卖问题
买卖股票问题(动规)
动态规划问题三部曲:
- 状态数组及状态转换(备忘录)(难点)。
- base case。就是刚开始时的特殊情况。
- 求解。
总体分为三类:
- 只允许交易一次。
- 允许交易无限次,含冷冻期,含手续费。
- 允许交易 k 次(最难)。
股票操作:买入、卖出、无操作。
股票状态:不持有、持有
状态数组定义:
- 只有一次交易或者没有限制交易次数的状态数组:
dp[i][0/1]:第 i 天不持有/持有股票的最大利润。
- 允许进行 k 次交易的状态数组:
dp[i][k][0/1]:第 i 天最多还能进行 k 次交易时不持有/持有股票的最大利润。
拓展:
- 如何对状态数组进行状态压缩?
121-买卖股票的最佳时机
代码:
public int maxProfit(int[] prices) {
int len = prices.length;
if (len < 2) {
return 0;
}
int[][] dp = new int[len][2];
// base case
dp[0][0] = 0;
dp[0][1] = -prices[0];
//状态转换。只允许交易一次,说明只会有某一天持有
for(int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
// 特殊情况,只进行一次交易,如果持有一定是当天买的。
dp[i][1] = Math.max(- prices[i], dp[i-1][1]);
// System.out.println(i + ": dp[i][0]=" + dp[i][0] + "; dp[i][1]= " + dp[i][1]);
}
// 求解
return dp[len - 1][0];
}
122-买卖股票的最佳时机 II
代码:
public int maxProfit(int[] prices) {
int len = prices.length;
if (len < 2) {
return 0;
}
int[][] dp = new int[len][2];
// base case
dp[0][0] = 0;
dp[0][1] = -prices[0];
// 状态转换。允许交易多次,说明持有天数没限制
for (int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][0] - prices[i], dp[i-1][1]);
// System.out.println(i + ": dp[i][0]=" + dp[i][0] + "; dp[i][1]= " + dp[i][1]);
}
// 求解
return dp[len-1][0];
}
123-买卖股票的最佳时机 III
代码:
public int maxProfit(int[] prices) {
int len = prices.length;
if (len < 2) {
return 0;
}
int max_k = 2;
int[][][] dp = new int[len][max_k+1][2];
// base case
// dp[i][0][0] = 0
for (int i = 0; i < len; i++) {
dp[i][0][0] = 0;
}
// dp[i][0][1] = Interger.minval
for (int i = 0; i < len; i++) {
dp[i][0][1] = Integer.MIN_VALUE;
}
// dp[0][k][0] = 0
for (int k = 0; k <= max_k; k++) {
dp[0][k][0] = 0;
}
// dp[0][k][1] = Integer.minval
// for (int k = 0; k <= max_k; k++) {
// dp[0][k][1] = prices[0];
// }
dp[0][0][1] = Integer.MIN_VALUE;
dp[0][1][1] = - prices[0];
dp[0][2][1] = - prices[0];
// dp 状态转换
for (int i = 1; i < len; i++) {
for (int k = 1; k <= max_k; k++) {
// 第2种情况:前面最多交易 k 次,这次卖出(一次完整交易)不计入交易次数
dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
// 第2种情况:前面最多交易 k-1 次,加上这次买入
dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
// System.out.println("i = " + i + "; k = " + k + ": dp[i][k][0]=" + dp[i][k][0] + "; dp[i][k][1]= " + dp[i][k][1]);
}
}
// 求解。最后一天,最多交易 k 次后不持有股票的最大利润。
return dp[len-1][max_k][0];
}
188-买卖股票的最佳时机 IV
代码:
public int maxProfit(int k, int[] prices) {
int len = prices.length;
if (len < 2) {
return 0;
}
// 这里 k 其实是有上限交易次数的
int max_k = Math.min(k, len / 2);
int[][][] dp = new int[len][max_k+1][2];
// base case
// dp[i][0][0] = 0
for (int i = 0; i < len; i++) {
dp[i][0][0] = 0;
}
// dp[i][0][1] = Interger.minval
for (int i = 0; i < len; i++) {
dp[i][0][1] = Integer.MIN_VALUE;
}
// dp[0][k][0] = 0
for (int ki = 0; ki <= max_k; ki++) {
dp[0][ki][0] = 0;
}
dp[0][0][1] = Integer.MIN_VALUE;
for (int ki = 1; ki <= max_k; ki++)
dp[0][ki][1] = - prices[0];
// dp 状态转换
for (int i = 1; i < len; i++) {
for (int ki = 1; ki <= max_k; ki++) {
dp[i][ki][0] = Math.max(dp[i-1][ki][0], dp[i-1][ki][1] + prices[i]);
dp[i][ki][1] = Math.max(dp[i-1][ki][1], dp[i-1][ki-1][0] - prices[i]);
System.out.println("i = " + i + "; ki = " + ki + ": dp[i][ki][0]=" + dp[i][ki][0] + "; dp[i][ki][1]= " + dp[i][ki][1]);
}
}
// 求解
return dp[len-1][max_k][0];
}
309-最佳买卖股票时机含冷冻期
代码:
public int maxProfit(int[] prices) {
int len = prices.length;
if (len < 2) {
return 0;
}
int[][] dp = new int[len][2];
// base case
dp[0][0] = 0;
dp[0][1] = -prices[0];
// 状态转换
for (int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
if (i == 1 ) {
// base case. dp[-1][1] 下标不存在,i 天购买,利润为 -prices[i]
dp[i][1] = Math.max(-prices[i], dp[i-1][1]);
} else {
// 当天进行交易的前一天冷冻
dp[i][1] = Math.max(dp[i - 2][0] - prices[i], dp[i - 1][1]);
}
System.out.println(i + ": dp[i][0]=" + dp[i][0] + "; dp[i][1]= " + dp[i][1]);
}
// 求解
return dp[len-1][0];
}
714-买卖股票的最佳时机含手续费
代码:
public int maxProfit(int[] prices, int fee) {
int len = prices.length;
if (len < 2) {
return 0;
}
int[][] dp = new int[len][2];
// base case
dp[0][0] = 0;
dp[0][1] = -prices[0] - fee;
// 状态转换
for (int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][0] - prices[i] - fee, dp[i-1][1]);
// System.out.println(i + ": dp[i][0]=" + dp[i][0] + "; dp[i][1]= " + dp[i][1]);
}
// 求解
return dp[len-1][0];
}
路径问题(动规)
62-不同路径
题意分析:
- m*n 的矩阵中,从左上角走到右下角有多少种走法。
解题思路:
- 动态规划。dp[i][j] 结果和 dp[i-1][j] / dp[i][j-1] 状态的关系。
注意点:
扩展:
代码:
/**
* 时间复杂度 O(m*n)
* 空间复杂读 O(m*n)
* 借助状态数组,增加空间复杂度。
*
* @param m
* @param n
* @return
*/
public int uniquePaths(int m, int n) {
// 状态数组
int[][] dp = new int[m][n];
// base case
dp[0][0] = 0;
for (int i = 0; i < m; i++) {
dp[i][0] = 1;
}
for (int i = 0; i < n; i++) {
dp[0][i] = 1;
}
// 状态转移
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i-1][j] + dp[i][j-1];
System.out.println(i + "-" + j + ": dp[i][j] = " + dp[i][j]);
}
}
return dp[m-1][n-1];
}
63-不同路径 II
题意分析:
- 加上不可达节点时的走法。
解题思路:
- 动态规划。
注意点:
- 初始化时要考虑障碍的初始值。
扩展:
代码:
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length, n = obstacleGrid[0].length;
// 状态数组。(数组初始化时值都为0)
int[][] dp = new int[m][n];
for (int i = 0; i < m; i++) {
System.out.println(Arrays.toString(dp[i]));
}
// base case.
// 这里的 && 判断很特殊,如果不满足直接退出 for 循环。
// for 循环改成 while 逻辑就好理解,相当于 while 循环直接 break 了
for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) {
dp[i][0] = 1;
}
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) {
dp[0][j] = 1;
}
// 状态转移
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (obstacleGrid[i][j] == 0)
dp[i][j] = dp[i-1][j] + dp[i][j-1];
// System.out.println(i + "-" + j + ": dp[i][j] = " + dp[i][j]);
}
}
// 求解
return dp[m-1][n-1];
}
64-最小路径和
题意分析:
- 无非就是把不同路径改成计算路径上节点的和,本质一样。
解题思路:
- 动态规划。
注意点:
- 和“不同路径”有点不一样,“不同路径”问题第一行/列的 base case 都是 1,base case 初始化时时要计算节点值的。
扩展:
代码:
public int minPathSum(int[][] grid) {
int m = grid.length, n = grid[0].length;
// 状态数组
int[][] dp = new int[m][n];
// base case,初始化时是要计算值的。
dp[0][0] = grid[0][0];
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i-1][0] + grid[i][0];
}
for (int j = 1; j < n; j++) {
dp[0][j] = dp[0][j-1] + grid[0][j];
}
// 状态转换
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
// 注意这里 +grid[i][j] 的值是都要加上的
dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
}
}
// 求解
return dp[m-1][n-1];
}
贪心问题
55-跳跃游戏
题意分析:
- 从数组下表开始跳跃,判断最终是否能够跳跃到数组最后一个节点。
解题思路:
- 贪心思路。每次找到当前位置可跳跃的最远长度,如果存在某个节点能够跳跃到最后,则直接返回 true。
- 时间复杂度:O(n)。最坏情况下需要遍历数组所有元素。
- 空间复杂度:O(1)。需要常量元素记录跳跃的最长距离。
- 动态规划。动态规划的核心就在dp状态数据的构建和转换过程的转换,动规分为只依赖前一个dp状态和依赖前面所有dp状态,本题属于后者。当前节点能否跳跃到最后节点,需要判断前面所有节点是否存在能够跳跃到最后节点的。
- 时间复杂度:O(nlog(n))。每个位置的dp数组依赖前面所有位置的dp信息。
- 空间复杂度:O(n)。需要额外的dp数组记录是否能够跳跃到最后节点。
注意点:
- 如何将第一种方法理解为贪心算法?
扩展:
代码:
/**
* 贪心算法,每次找到当前位置能 jump 的最大距离
* @param nums
* @return
*/
public boolean canJump(int[] nums) {
int n = nums.length;
int fastest = 0;
for (int i = 0; i < n - 1; i++) {
fastest = Math.max(fastest, i + nums[i]);
System.out.println("fastest = " + fastest + "; i = " + i);
if (fastest <= i) return false;
}
return fastest >= n - 1;
}
/**
* 动态规划。
* 1、dp 数组。记录每个位置能否到达最后位置。
* 2、状态转换。什么情况下会触发状态转换。
* 3、base case。
* 4、求解。dp[len - 1]
* @param nums
* @return
*/
public boolean canJump(int[] nums) {
int n = nums.length;
// 状态数组。记录每个节点是否能够跳跃到最后节点
boolean[] dp = new boolean[n];
// base case
dp[0] = true;
for (int i = 1; i < n; i++) {
// 判断从 dp[0~i-1] 中能够跳跃到最后节点的节点开始(即当前状态和前面多个状态有关)
for (int j = 0; j < i; j++) {
// 判断前面所有位置时,一个条件是前面位置能够到达,另一个条件是能跳过当前节点
if (dp[j] && j + nums[j] >= i) {
dp[i] = true;
break;
}
}
// System.out.println("i = " + i + "; dp[i] = " + dp[i]);
}
// 求解(动规需要遍历完整个数组)
return dp[n - 1];
}
*45-跳跃游戏 II
题意分析:
解题思路:
- 贪心算法。每个更新跳跃区间,区间更新即为一次 jump,直到区间中包含最后一个位置(索引),表示该区间是上一跳的最远距离,此时结束即可。
- 优化后的贪心。每次只更远跳跃的最远距离(即只保留右区间),如果抵达了右区间,则更新最区间距离。
注意点:
扩展:
代码:
/**
* 贪心算法:每次更新跳跃区间
* @param nums
* @return
*/
public int jump(int[] nums) {
int n = nums.length;
int start = 0, end = 0; // [0,0]
int fastest = 0;
int step = 0;
while (end < n - 1) {
// 每次能跳跃的区间
for (int i = start; i <= end; i++) {
fastest = Math.max(fastest, i + nums[i]);
}
// 更新区间左右值
start = end + 1;
end = fastest;
step++;
System.out.println("start = " + start + "; end = " + end);
}
return step;
}
/**
* 优化后的贪心算法,每次更新跳跃到最远距离
* @param nums
* @return
*/
public int jump2(int[] nums) {
int n = nums.length;
int fastest = 0;
// 跳跃最远位置的 index
int end = 0;
int step = 0;
for (int i = 0; i < n - 1; i++) {
// 每次能跳跃的最远距离
fastest = Math.max(fastest, i + nums[i]);
// 到达跳跃最远距离的边界
if (end == i) {
end = fastest;
step++;
}
System.out.println("i = " + i + "; fastest = " + fastest + "; end = " + end + "; step = " + step);
}
return step;
}
134-加油站
题意分析:
- 从某个位置出发,根据加油和耗油判断能否回到远点。
解题思路:
- 暴力解法。直接判断每个节点能否回到远点。
- 时间复杂度:O(n^2)
- 贪心解法。转换为一维数组,判断从某个位置出发,走到的任何节点油箱容量均大于等于0。
注意点:
扩展:
代码:
/**
* 暴力破解:
* 遍历每个 gas[i] >= cost[i] 的 gas,判断是否能够回到原点。
* @param gas
* @param cost
* @return
*/
public int canCompleteCircuit2(int[] gas, int[] cost) {
int n = gas.length;
for (int start = 0; start < n; start++) {
if (gas[start] < cost[start]) {
continue;
}
// 每次从新的节点出发油箱容量 tank 均为0
int tank = 0;
for (int step = 0; step < n; step++) {
// 表示每次从 start 节点出发
int i = (start + step) % n;
tank += gas[i];
tank -= cost[i];
if (tank < 0) break;
}
if (tank >= 0) return start;
}
return -1;
}
/**
* 贪心解法
* 1、把 gas[i] - cost[i] 组成一个新的数组,
* 目标是找到一个起点,出发一圈后回到起点的过程中油箱容量 tank 一直大于等于0。
* gas=[1,2,3,4,5], cost=[3,4,5,1,2] 问题转换为 gap = [-2, -2, -2, 3, 3] 问题。
* 2、如果从 i 走到 j 出现 tank < 0,那么从 [i,j] 之间任意一个节点出发 tank 都小于 0。
* @param gas
* @param cost
* @return
*/
public int canCompleteCircuit(int[] gas, int[] cost) {
int n = gas.length;
int[] gap = new int[n];
int sum = 0;
for (int i = 0; i < n; i++) {
gap[i] = gas[i] - cost[i];
sum += gap[i];
}
if (sum < 0) {
return -1;
}
System.out.println(Arrays.toString(gap));
int tank = 0, start = 0;
for (int i = 0; i < n; i++) {
tank += gap[i];
// i 走到 j 不满足条件,那么 [i,j] 直接都不满足
if (tank < 0) {
tank = 0;
start = i + 1;
}
}
return start;
}
区间问题(贪心)
435-无重叠区间(贪心+排序/动态+二分)
题意分析:
- 求出最大无重复区间,然后剩下的就是重复区间。
解题思路:
- 贪心算法。贪心算法求区间问题每次都是先对区间右边界排序,然后比较当前左区间与上一区间的右边界,贪心之处就在于之比较最大值(或最远值,比如跳跃游戏)。
- 动态规划。动态规划求区间问题需要先对左区间排序,然后比较当前区间左边界与上一区间右边界是否重叠,逐一求每个区间的最大无重叠区间。
注意点:
扩展:
代码:
/**
* 贪心思想。
* 1、先对区间右边界 end 升序排序
* 2、比较当前区间右边界与下一区间的左边界,不重叠则不重叠区间+1
* @param intervals
* @return
*/
public int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[1] - o2[1];
}
});
for (int i = 0; i < intervals.length; i++) {
System.out.println(Arrays.toString(intervals[i]));
}
int ans = 1;
int end = intervals[0][1];
for (int i = 1; i < intervals.length; i++) {
// start >= end
if (intervals[i][0] >= end) {
ans++;
end = intervals[i][1];
}
}
return intervals.length - ans;
}
/**
* 动态规划(超出时间限制)
* 1、先按区间 start 排序(为了方便当前区间 start 与上一区间 end 判断)
* 2、dp[i]:每个一元数组的最大无重叠子区间
* 3、当前状态与前面多个状态均有关
* @param intervals
* @return
*/
public int eraseOverlapIntervals2(int[][] intervals) {
int n = intervals.length;
// 状态数据
int[] dp = new int[n];
// base case:默认都有一个无重叠区间
Arrays.fill(dp, 1);
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[0] - o2[0];
}
});
for (int i = 0; i < intervals.length; i++) {
System.out.println(Arrays.toString(intervals[i]));
}
// 状态转换
for (int i = 0; i < n; i++) {
// dp 数组和前面多个状态有关
for (int j = 0; j < i; j++) {
// 更新重叠子区间条件判断
if (intervals[i][0] >= intervals[j][1]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
// 求解
return n - Arrays.stream(dp).max().getAsInt();
}
452-用最少数量的箭引爆气球-无重复区间(贪心+排序)
题意分析:
- 重复区间用一支箭射穿即可,说明问题是要求无重复区间的数量。
解题思路:
- 贪心+排序。问题本质就是求最大无重复区间的数量,每个无重复区间用一支箭射穿即可。
注意点:
- 排序时直接返回 o1[1] - o2[1] 时要考虑减法的结果是否超过 INT 限制,会导致排序结果混乱。
扩展:
代码:
/**
* 贪心+排序。
* 问题本质就是求最大无重复区间的数量,每个无重复区间用一支箭射穿即可。
* @param points
* @return
*/
public int findMinArrowShots(int[][] points) {
int n = points.length;
// 按区间end升序排序
Arrays.sort(points, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
// 排序时直接返回 o1[1]-o2[1] 要考虑返回结果是否超出INT限制
if (o1[1] > o2[1])
return 1;
else if (o1[1] < o2[1])
return -1;
else
return 0;
}
});
int end = points[0][1];
int ans = 1;
for (int i = 1; i < n; i++) {
// 边界如果相等则可以处理两个区间,所以这里不用等于号
if (points[i][0] > end) {
ans++;
end = points[i][1];
}
}
return ans;
}
1024-视频拼接(贪心+排序)
题意分析:
- 选取最少的区间数,使得区间完全包含 [0,T] 。
解题思路:
- 贪心+排序。[0,1], [0,5], [0,9] 按照贪心策略,肯定是优先选择 [0,9] 这个大区间,才可能使得最终选择的区间数最少(这便是贪心最有选择策略)。所以这里需要先对数组排序,先按 start 升序排序,再按 end 降序排序。然后判断区间的包含关系更新区间。
注意点:
- 要注意这里区间可能划分小了,比如 [0,4], [2,6], [4,7],两个区间能解决的事就不用三个区间解决,因为还要判断上个区间的结束 pre_end 值,便于更新区间数统计。
扩展:
- 能否打印出对应的区间?
代码:
public int videoStitching(int[][] clips, int time) {
int n = clips.length;
// 对数组进行排序,先根据 start 升序排序,再按照 cur_end 降序排序
Arrays.sort(clips, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
if (o1[0] == o2[0])
return o2[1] - o1[1];
return o1[0] - o2[0];
}
});
for (int i = 0; i < n; i++) {
System.out.println(Arrays.toString(clips[i]));
}
int step = 1;
int pre_end = 0, cur_end = clips[0][1];
// 无法从 0 左区间开始,直接返回无法剪辑
if (clips[0][0] > 0 ) return -1;
// 如果第一个区间满足要求,则直接返回
if (cur_end >= time) return step;
LinkedList<int[]> list = new LinkedList<>();
list.add(new int[]{clips[0][0], clips[0][1]});
for (int i = 1; i < n; i++) {
// 下一区间跨越上一区间的end边界
if (clips[i][0] <= cur_end && clips[i][1] > cur_end) {
// step 更新条件很关键。比如 [0,4],[2,6],[4,7],[6,9],其实只要三个区间就能满足要求 [2,6]是多余的
// 因此,这里要记录上一次区间的结束 pre_end 值,只有超过 pre_end 值才更新 step
if (clips[i][0] > pre_end) {
step++;
pre_end = cur_end;
} else {
// 说明上一次区间冗余,需要删除掉
list.pollLast();
}
cur_end = clips[i][1];
list.add(new int[]{clips[i][0], clips[i][1]});
if (cur_end >= time) break;
}
}
if (cur_end >= time) return step;
return -1;
}
1288-删除被覆盖区间(区间覆盖)
题意分析:
- 对多个区间中包含的区间进行去重,只保留不重复且区间范围大的区间。
解题思路:
- 排序+贪心。先对区间 start 升序和 end 降序,然后画图确认区间覆盖的条件进行过滤。
注意点:
扩展:
代码:
/**
* 区间覆盖问题
* 1、排序。先对区间 start 升序排序,再对区间 end 降序排序。
* 2、画图。判断什么时候覆盖什么是不覆盖。
* @param intervals
* @return
*/
public int removeCoveredIntervals(int[][] intervals) {
int n = intervals.length;
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
if (o1[0] != o2[0])
return o1[0] - o2[0];
return o2[1] - o1[1];
}
});
for (int i = 0; i < n; i ++) {
System.out.println(Arrays.toString(intervals[i]));
}
int left = Integer.MAX_VALUE, right = Integer.MIN_VALUE;
int res = 0;
for (int i = 0; i < n; i++) {
if (!(intervals[i][0] >= min && intervals[i][1] <= max)) {
res++;
left = intervals[i][0];
right = intervals[i][1];
// System.out.println(left + "; " + right);
}
}
return res;
}
56-合并区间(区间合并)
题意分析:
- 对存在重叠或连续的区间进行合并,合并成一个大的区间。
解题思路:
- 排序+贪心。先对区间 start 升序和 end 降序,然后画图确认区间合并的条件进行过滤。
注意点:
- 返回二维数组的数据结构选取,以及如何返回结果。
扩展:
代码:
/**
* 区间合并问题
* 1、排序。先对区间 start 升序排序,再对区间 end 降序排序。
* 2、画图。判断什么时候需要更新合并 list,什么时候更新 list 右区间。
* 3、数据结构选取。List<int[]> list,返回 list.toArray(new int[list.size][]);
* @param intervals
* @return
*/
public int[][] merge(int[][] intervals) {
int n = intervals.length;
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
if (o1[0] != o2[0])
return o1[0] - o2[0];
return o2[1] - o1[1];
}
});
// for (int i = 0; i < n; i ++) {
// System.out.println(Arrays.toString(intervals[i]));
// }
int left = 0, right = 0;
List<int[]> list = new ArrayList<>();
for (int i = 0; i < n; i++) {
left = intervals[i][0];
right = intervals[i][1];
// 不需要进行合并操作的情况
if (list.size() == 0 || list.get(list.size() - 1)[1] < left) {
list.add(new int[]{left, right});
}
if (left <= list.get(list.size() - 1)[1] && right >= list.get(list.size() - 1)[1]) {
// 更新右区间
list.get(list.size() - 1)[1] = right;
}
}
return list.toArray(new int[list.size()][]);
}
986-区间列表交集(区间交集)
题意分析:
- 求两个区间的交集,单个元素存在交集也要输出。
解题思路:
- 双指针。排序+画图,由于区间已排序,直接画图通过两个指针分别对比两个区间的元素,如果存在交集则添加到 list,如果没交集则左右指针依次前进。
注意点:
- 左右指针的前进条件,什么时候前进 left,什么时候前进 right。
- while 循环的退出条件,如果有一个指针扫描区间结束,则一定不存在交集。
扩展:
代码:
/**
* 区间已排序,直接画图判断更新交集区间。
* @param firstList
* @param secondList
* @return
*/
public int[][] intervalIntersection(int[][] firstList, int[][] secondList) {
int m = firstList.length, n = secondList.length;
List<int[]> list = new ArrayList<>();
int left = 0, right = 0;
while (left < m && right < n) {
// 取两个区间的左右值
int low = Math.max(firstList[left][0], secondList[right][0]);
int high = Math.min(firstList[left][1], secondList[right][1]);
// 区间存在交集
if (low <= high) {
list.add(new int[]{low, high});
}
if (firstList[left][1] < secondList[right][1]) {
left++;
} else {
right++;
}
}
return list.toArray(new int[list.size()][]);
}
未分类
70-爬楼梯
题意分析:
解题思路:
注意点:
扩展:
代码:
public int climbStairs(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
// 状态数组
int[] dp = new int[n+1];
// base case. dp[0] ignore
dp[1] = 1;
dp[2] = 2;
// 状态转换
for (int i = 3; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
// 求解
return dp[n];
}
模版
题意分析:
解题思路:
注意点:
扩展:
代码: