1 简介
-
本文主要是基于labuladong大佬资料的学习笔记
-
包含递归思想,贪心只是动态规划的一个特例
-
一般分为自底向上和自上到下的写法,注意
-
穷举有重叠时候考虑建立备忘录。
-
明确「状态」 -> 定义 dp 数组/函数的含义 -> 明确「选择」-> 明确 base case。
-
一维的dp 数组存储的信息还不够,不足以推出下一步的答案,需要把 dp 数组扩大成二维数组甚至三维数组。
那么,如果仔细观察的话可以发现其中的原因的。你只要把住两点就行了:
-
1、遍历的过程中,所需的状态必须是已经计算出来的。
-
2、遍历的终点必须是存储结果的那个位置
2 简单的动态规划
2.1斐波那契数列
解法一:递归
int fib(int N) {
if (N == 1 || N == 2) return 1;
return fib(N - 1) + fib(N - 2);
}
解法二:带备忘录的递归,自上到下
int fib(int N) {
if (N < 1) return 0;
// 备忘录全初始化为 0
vector<int> memo(N + 1, 0);
// 初始化最简情况
return helper(memo, N);
}
int helper(vector<int>& memo, int n) {
// base case
if (n == 1 || n == 2) return 1;
// 已经计算过
if (memo[n] != 0) return memo[n];
memo[n] = helper(memo, n - 1) +
helper(memo, n - 2);
return memo[n];
}
解法三:带备忘录的dp数组,自下到上
int fib(int N) {
vector<int> dp(N + 1, 0);
// base case
dp[1] = dp[2] = 1;
for (int i = 3; i <= N; i++)
dp[i] = dp[i - 1] + dp[i - 2];
return dp[N];
}
解法四:dp的状态压缩(之和相邻状态有关),自下到上
int fib(int n) {
if (n == 2 || n == 1)
return 1;
int prev = 1, curr = 1;
for (int i = 3; i <= n; i++) {
int sum = prev + curr;
prev = curr;
curr = sum;
}
return curr;
}
2.2 凑零钱
问题:给你k种面值的硬币,面值分别为c1, c2 … ck,每种硬币的数量无限,再给一个总金额amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。
解法一:自上到下备忘录
ef coinChange(coins: List[int], amount: int):
# 备忘录
memo = dict()
def dp(n):
# 查备忘录,避免重复计算
if n in memo: return memo[n]
if n == 0: return 0
if n < 0: return -1
res = float('INF')
for coin in coins:
subproblem = dp(n - coin)
if subproblem == -1: continue
res = min(res, 1 + subproblem)
# 记入备忘录
memo[n] = res if res != float('INF') else -1
return memo[n]
return dp(amount)
解法二:自下到上备忘录
int coinChange(vector<int>& coins, int amount) {
// 数组大小为 amount + 1,初始值也为 amount + 1
vector<int> dp(amount + 1, amount + 1);
// base case
dp[0] = 0;
for (int i = 0; i < dp.size(); i++) {
// 内层 for 在求所有子问题 + 1 的最小值
for (int coin : coins) {
// 子问题无解,跳过
if (i - coin < 0) continue;
dp[i] = min(dp[i], 1 + dp[i - coin]);
}
}
return (dp[amount] == amount + 1) ? -1 : dp[amount];
}
2.3 打家劫舍
不打劫相邻的,样例:
输入:[1,2,3,1]
输出:
4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
解法一:自上而下+备忘录
private int[] memo;
// 主函数
public int rob(int[] nums) {
// 初始化备忘录
memo = new int[nums.length];
Arrays.fill(memo, -1);
// 强盗从第 0 间房子开始抢劫
return dp(nums, 0);
}
// 返回 dp[start..] 能抢到的最大值
private int dp(int[] nums, int start) {
if (start >= nums.length) {
return 0;
}
// 避免重复计算
if (memo[start] != -1) return memo[start];
int res = Math.max(dp(nums, start + 1),
nums[start] + dp(nums, start + 2));
// 记入备忘录
memo[start] = res;
return res;
}
解法二:自下而上+备忘录
类似于反过来的跳台阶,从后往前扫
int rob(int[] nums) {
int n = nums.length;
// dp[i] = x 表示:
// 从第 i 间房子开始抢劫,最多能抢到的钱为 x
// base case: dp[n] = 0
int[] dp = new int[n + 2];
for (int i = n - 1; i >= 0; i--) {
dp[i] = Math.max(dp[i + 1], nums[i] + dp[i + 2]);
}
return dp[0];
}
解法三:相邻状态有关+状态压缩+自下而上
int rob(int[] nums) {
int n = nums.length;
// 记录 dp[i+1] 和 dp[i+2]
int dp_i_1 = 0, dp_i_2 = 0;
// 记录 dp[i]
int dp_i = 0;
for (int i = n - 1; i >= 0; i--) {
dp_i = Math.max(dp_i_1, nums[i] + dp_i_2);
dp_i_2 = dp_i_1;
dp_i_1 = dp_i;
}
return dp_i;
}
变形1:这些房子不是一排,而是围成了一个圈
分析:最后一个和最先一个不能同时存在,两周情况的最值
public int rob(int[] nums) {
int n = nums.length;
if (n == 1) return nums[0];
return Math.max(robRange(nums, 0, n - 2),
robRange(nums, 1, n - 1));
}
// 仅计算闭区间 [start,end] 的最优结果
int robRange(int[] nums, int start, int end) {
int n = nums.length;
int dp_i_1 = 0, dp_i_2 = 0;
int dp_i = 0;
for (int i = end; i >= start; i--) {
dp_i = Math.max(dp_i_1, nums[i] + dp_i_2);
dp_i_2 = dp_i_1;
dp_i_1 = dp_i;
}
return dp_i;
}
变形2:房子在二叉树的节点上,相连的两个房子不能同时被抢劫
分析:相邻结点不相加
Map<TreeNode, Integer> memo = new HashMap<>();
public int rob(TreeNode root) {
if (root == null) return 0;
// 利用备忘录消除重叠子问题
if (memo.containsKey(root))
return memo.get(root);
// 抢,然后去下下家
int do_it = root.val
+ (root.left == null ?
0 : rob(root.left.left) + rob(root.left.right))
+ (root.right == null ?
0 : rob(root.right.left) + rob(root.right.right));
// 不抢,然后去下家
int not_do = rob(root.left) + rob(root.right);
int res = Math.max(do_it, not_do);
memo.put(root, res);
return res;
}
优化
int rob(TreeNode root) {
int[] res = dp(root);
return Math.max(res[0], res[1]);
}
/* 返回一个大小为 2 的数组 arr
arr[0] 表示不抢 root 的话,得到的最大钱数
arr[1] 表示抢 root 的话,得到的最大钱数 */
int[] dp(TreeNode root) {
if (root == null)
return new int[]{0, 0};
int[] left = dp(root.left);
int[] right = dp(root.right);
// 抢,下家就不能抢了
int rob = root.val + left[0] + right[0];
// 不抢,下家可抢可不抢,取决于收益大小
int not_rob = Math.max(left[0], left[1])
+ Math.max(right[0], right[1]);
return new int[]{not_rob, rob};
}
3 子序列问题
- 子序列:不连续
- 字串:连续
满足条件的不连续子序列问题
3.1 编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
输入:word1 = “horse”, word2 = “ros”
输出:3
解释: horse -> rorse (将 ‘h’ 替换为 ‘r’) rorse -> rose (删除 ‘r’) rose -> ros (删除 ‘e’)
分析:
- 两个字符同时从后往前扫描
- 相等则跳过不算操作数。
- 不等考虑三种操作:str1 插入,操作数+1;str1删除,操作数+1;str1替换成str2对应字符,操作数+1。
具体分析
if s1[i] == s2[j]:
啥都别做(skip)
i, j 同时向前移动
else:
三选一:
插入(insert)
删除(delete)
替换(replace)
优化一: 备忘录优化
def minDistance(s1, s2) -> int:
memo = dict() # 备忘录
def dp(i, j):
if (i, j) in memo:
return memo[(i, j)]
...
if s1[i] == s2[j]:
memo[(i, j)] = ...
else:
memo[(i, j)] = ...
return memo[(i, j)]
return dp(len(s1) - 1, len(s2) - 1)
优化二: DP table优化⭐⭐
题目中我给的解法:DP table优化
public int minDistance(String word1, String word2) {
int len = word1.length();
int len2 = word2.length();
// dp[i][j]: str 1~i和str 1~j的最大距离,dp
// 从1开始一方面方便理解,一方面防止越界
int[][] dp = new int[len + 1][len2 + 1];
// base case:i/j ==0 :dp[j/i] = j/i操作数
for(int i = 1 ; i<=len; i++){
dp[i][0] = i;
}
for(int i = 1 ; i<=len2; i++){
dp[0][i] = i;
}
// 状态传递
for(int i = 1; i <=len ; i++){
for(int j = 1 ; j <= len2 ; j++){
if(word1.charAt(i-1) == word2.charAt(j-1)){
dp[i][j] = dp[i-1][j-1];//无操作,返回之前的操作数
}else {
dp[i][j] = Math.min(
dp[i - 1][j] + 1, //删除
Math.min(
dp[i][j - 1] + 1, //插入
dp[i - 1][j - 1] + 1 //赋值
)
);
}
}
}
return dp[len][len2];
}
优化三: 相邻三状态有关,状态压缩
3.2 最长递增子序列LIS⭐
详解
问题
分析1:
- dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。
- 则dp[i] = max(在i之前存在nums[j] < nums[i] 中最大的dp[j]+1,1)
- base case:第一次出现dp[i] = 1
分析2:
分发扑克牌+二分查找
代码
变形1:
最长递增字串。
dp的定义为0~i位置的最长递增字串的长度,当 nums[i]>nums[i-1]时候dp[i] = dp[i-1]+1。
3.3 信封嵌套问题
问题:
分析:
- 最长递增子序列LIS的变形
- 要求每组数组的首位都要满足递增子序列
代码:
LIS见上一节
3.4 最大子串和
连续子数组的最大和
问题
分析
- nums[0…i]中的「最大的子数组和」为dp[i]。
代码:
int maxSubArray(int[] nums) {
int n = nums.length;
if (n == 0) return 0;
int[] dp = new int[n];
// base case
// 第一个元素前面没有子数组
dp[0] = nums[0];
// 状态转移方程
for (int i = 1; i < n; i++) {
dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]);
}
// 得到 nums 的最大子数组
int res = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
res = Math.max(res, dp[i]);
}
return res;
}
状态压缩:
int maxSubArray(int[] nums) {
int n = nums.length;
if (n == 0) return 0;
// base case
int dp_0 = nums[0];
int dp_1 = 0, res = dp_0;
for (int i = 1; i < n; i++) {
// dp[i] = max(nums[i], nums[i] + dp[i-1])
dp_1 = Math.max(nums[i], nums[i] + dp_0);
dp_0 = dp_1;
// 顺便计算最大的结果
res = Math.max(res, dp_1);
}
return res;
}
3.5 最长公共子序列LCS⭐
详情
leetcode1143
问题:
给你输入两个字符串s1和s2,请你找出他们俩的最长公共子序列,返回这个子序列的长度。
比如说输入s1 = “zabcde”, s2 = “acez”,它俩的最长公共子序列是lcs = “ace”,长度为 3,所以算法返回 3。
分析
- 自上而下分析:dp(s1, i, s2, j)计算s1[i…]和s2[j…]的最长公共子序列长度。
// 定义:计算 s1[i..] 和 s2[j..] 的最长公共子序列长度
int dp(String s1, int i, String s2, int j) {
if (s1.charAt(i) == s2.charAt(j)) {
return 1 + dp(s1, i + 1, s2, j + 1)
} else {
// s1[i] 和 s2[j] 中至少有一个字符不在 lcs 中,
// 穷举三种情况的结果,取其中的最大结果
return max(
// 情况一、s1[i] 不在 lcs 中
dp(s1, i + 1, s2, j),
// 情况二、s2[j] 不在 lcs 中
dp(s1, i, s2, j + 1),
// 情况三、都不在 lcs 中
dp(s1, i + 1, s2, j + 1)
);
}
}
代码
- 自下而上:定义dp[i][j] 是 s1[…i] 和s2[…j]LCS
int longestCommonSubsequence(String s1, String s2) {
int m = s1.length(), n = s2.length();
int[][] dp = new int[m + 1][n + 1];
// 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j]
// 目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n]
// base case: dp[0][..] = dp[..][0] = 0
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 现在 i 和 j 从 1 开始,所以要减一
if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
// s1[i-1] 和 s2[j-1] 必然在 lcs 中
dp[i][j] = 1 + dp[i - 1][j - 1];
} else {
// s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中
dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
}
}
}
return dp[m][n];
}
变形1:删除使两字符串相等的操作数
先求lcs,每个字符串减去lcs长度,相加就是总操作数。
变形2:删除使其相等的ASCII的操作数之和
- 删除的字符的 ASCII 码加起来是多少
- 计算lcs长度时,如果一个字符串为空,那么lcs长度必然是 0;但是这道题如果一个字符串为空,另一个字符串必然要被全部删除,所以需要计算另一个字符串所有字符的 ASCII 码之和。
变形3:包含另一个子序列的方案数
输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下图所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
(上箭头符号 ^ 表示选取的字母)
rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^
分析:
-
dp【i】【j】表示 s 1~i 包含 t 1~j的多少方案
-
当 S[j] == T[i] , dp[i][j] = dp[i-1][j-1] + dp[i-1][j]; 考虑s的i是否用不用,用的话抵消掉,不用的话去找之前匹配的方案
-
当 S[j] != T[i] , dp[i][j] = dp[i-1][j] 不相等s的i肯定不能用了,找之前的方案,按顺序的找
class Solution {
public int numDistinct(String s, String t) {
int len1 = s.length();
int len2 = t.length();
//dp【i】【j】表示 s 1~i 包含 t 1~j的多少方案
int[][] dp = new int[len1 + 1][len2 + 1];
//base case
for(int i = 0; i <= len2 ;i++){
dp[0][i] = 0;
}
for(int j = 0; j <= len1; j++){
dp[j][0] = 1;
}
// 自底向上遍历dp
for(int i = 1 ;i <=len1 ;i++){
for(int j = 1; j <= len2;j++){
if(s.charAt(i-1) == t.charAt(j-1)){
//相等时考虑用不用i,用的话会匹配掉,不用的话去匹配j-1
dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
}else{
//不相等的话只能舍弃i,也考虑了匹配的顺序性质。
dp[i][j] = dp[i-1][j];
}
}
}
return dp[len1][len2];
}
}
3.6 最长回文子序列
分析
- dp[i][j]最长回文子序列的长度
- 状态转移思路
if (s[i] == s[j])
// 它俩一定在最长回文子序列中
dp[i][j] = dp[i + 1][j - 1] + 2;
else
// s[i+1..j] 和 s[i..j-1] 谁的回文子序列更长?
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
- 两种遍历方式:
代码
int longestPalindromeSubseq(string s) {
int n = s.size();
// dp 数组全部初始化为 0
vector<vector<int>> dp(n, vector<int>(n, 0));
// base case
for (int i = 0; i < n; i++)
dp[i][i] = 1;
// 反着遍历保证正确的状态转移
for (int i = n - 1; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
// 状态转移方程
if (s[i] == s[j])
dp[i][j] = dp[i + 1][j - 1] + 2;
else
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
}
// 整个 s 的最长回文子串长度
return dp[0][n - 1];
}
3.7 最长回文子串
法一
- 分奇偶中心点
- 依次从前往后扫描
法二
- 动态规划
这个问题可以用动态规划方法解决,时间复杂度一样,但是空间复杂度至少要 O(N^2) 来存储 DP table。这道题是少有的动态规划非最优解法的问题。
4 0-1背包
问题
详情
-
给你一个可装载重量为W的背包和N个物品,每个物品有重量和价值两个属性。其中第i个物品的重量为wt[i],价值为val[i],现在让你用这个背包装物品,最多能装的价值是多少?
-
举个简单的例子,输入如下:
背包:N = 3, W = 4
物品的重量 wt = [2, 1, 3]
物品的价值val = [4, 2, 3]
分析
- 要求最大价值(因变量),限制因素为重量,数量(自变量)。
- dp【i】【m】定位为前i个物品的不超过m重量的最大价值
- 递推可行性分析:
当使用到第i个物品时候,由于m的限制可能用不了,此时只能不用他,最大价值和用不用他没啥关系,则有dp[i][m]=dp[i-1][m]。
第二种情况用上第i个物品重量也不超m,可以用他,那此时的最大价值就要在之前的一个状态的而基础上加上i物品的价值,之前的那个状态就是不用他的那个最大价值,即dp[i][m] = dp[i-1][m-wt[i]]+val[i] - base case很好理解,i == 0 没有物品和m == 0没有重量,价值肯定是零。
代码
int knapsack(int W, int N, vector<int>& wt, vector<int>& val) {
// vector 全填入 0,base case 已初始化
vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= N; i++) {
for (int w = 1; w <= W; w++) {
//防止数组越界
if (w - wt[i-1] < 0) {
// 当前背包容量装不下,只能选择不装入背包
dp[i][w] = dp[i - 1][w];
} else {
// 装入或者不装入背包,择优
dp[i][w] = max(dp[i - 1][w - wt[i-1]] + val[i-1],
dp[i - 1][w]);
}
}
}
return dp[N][W];
}
4.1 背包变体1——分割等和子集
详情
问题
分析
- 两个等和的包。从一个数组中拿物品。
- 给一个可装载重量为sum/2的背包和N个物品,每个物品的重量为nums[i]。
- 限制条件:数量和重量,声明dp[i][j] = x,对于前i个物品,当前背包的容量为j时,若x为true,则说明可以恰好将背包装满,若x为false,则说明不能恰好将背包装满。
- 前i个数正好和为j的真假:i可以用可以不用,用的时候,dp[i][j] = dp[i-1][j-nums[i]],不能用的话,只能取决于之前的状态,dp[i][j] = dp[i-1][j],不管能不能用,只要一个满足他就为真
代码
bool canPartition(vector<int>& nums) {
int sum = 0;
for (int num : nums) sum += num;
// 和为奇数时,不可能划分成两个和相等的集合
if (sum % 2 != 0) return false;
int n = nums.size();
sum = sum / 2;
vector<vector<bool>>
dp(n + 1, vector<bool>(sum + 1, false));
// base case
for (int i = 0; i <= n; i++)
dp[i][0] = true;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= sum; j++) {
if (j - nums[i - 1] < 0) {
// 背包容量不足,不能装入第 i 个物品
dp[i][j] = dp[i - 1][j];
} else {
// 装入或不装入背包
dp[i][j] = dp[i - 1][j] | dp[i - 1][j-nums[i-1]];
}
}
}
return dp[n][sum];
}
状态压缩优化
注意j的倒序
bool canPartition(vector<int>& nums) {
int sum = 0, n = nums.size();
for (int num : nums) sum += num;
if (sum % 2 != 0) return false;
sum = sum / 2;
vector<bool> dp(sum + 1, false);
// base case
dp[0] = true;
for (int i = 0; i < n; i++)
for (int j = sum; j >= 0; j--)
if (j - nums[i] >= 0)
dp[j] = dp[j] || dp[j - nums[i]];
return dp[sum];
}
4.2 背包变体2——完全背包(重复使用组合)
详情
问题
物品重复使用(不考虑组合顺序,即组合问题)
分析
- 限制条件:个数和总数 待求方法数。
- 递归可行性,使用i个硬币时候方法数,和之前的不用他的方法数有关
- dp[i][j]:前i个硬币组成j金额的方法数,那么如果用i的硬币的话,那么此时的方法数应该是dp[i][j-nums[i]],抵消掉该硬币的使用价值,也可用i,所以不是i-1;如果不用i的话,那就由dp[i-1][j],之前的i-1个硬币来凑j吧。
代码
int change(int amount, int[] coins) {
int n = coins.length;
int[][] dp = amount int[n + 1][amount + 1];
// base case
for (int i = 0; i <= n; i++)
dp[i][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= amount; j++)
if (j - coins[i-1] >= 0)
dp[i][j] = dp[i - 1][j]
+ dp[i][j - coins[i-1]];
else
dp[i][j] = dp[i - 1][j];
}
return dp[n][amount];
}
状态压缩
int change(int amount, int[] coins) {
int n = coins.length;
int[] dp = new int[amount + 1];
dp[0] = 1; // base case
//用到coin的方案计算
for (int coin:conis)
for (int j = 1; j <= amount; j++)
if (j - coin >= 0)
dp[j] += dp[j-coin];
return dp[amount];
}
4.3 背包变体2——完全背包(重复使用排列)
问题
- leetcode377
- 给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。题目数据保证答案符合 32 位整数范围。
示例 1:
输入:nums = [1,2,3], target = 4
输出:7
解释: 所有可能的组合为:
(1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1) 请注意,顺序不同的序列被视作不同的组合。
分析
重复使用,并考虑组合顺序,排列
- dp[i][j]定义为长度为i,和为j的组合种类。
- 需要考虑最后一位取得是哪个num,dp[i][j] = dp[i-1][j-num1]+dp[i-1][j-num2]+…+dp[i-1][j-numn]
- 排列与组合不同的是,循环在内部还是外部:
- 如果是排列,决策需要考虑每个数字 i 用分别可以用哪些num(硬币),所以 i 的遍历在外部
- 如果是组合,决策就只需要考虑哪个 num(硬币)可用,所以 num 的遍历在外部,用到哪个计算哪个,减去当前的就是之前硬币组成的方案数。
代码
public int combinationSum4(int[] nums, int target) {
int len = nums.length;
//f[i][j] 为组合长度为 ii,凑成总和为 jj 的方案数是多少
int[][] dp = new int[len + 1][target+1];
dp[0][0] = 1;
int res = 0;
for(int i = 1 ;i <= target;i++){
for(int j = 0 ;j <= target;j++ ){
for(int num:nums){
//排列问题,考虑最后一位用不用前i个的
if(j >= num){
dp[i][j] += dp[i-1][j-num];
}
}
}
res+=dp[i][target];
}
return res;
}
状态压缩优化
public int combinationSum5(int[] nums, int target) {
int len = nums.length;
//f[j] 不限制长度,凑成总和为 j 的方案数是多少
int[] dp = new int[target+1];
dp[0] = 1;
for(int j = 1 ;j <= target;j++ ){
for(int num:nums){
//排列问题,考虑最后一位用不用前i个的
if(j >= num){
dp[j] += dp[j-num];
}
}
}
return dp[target];
}
5 股票买卖
6 贪心算法
- 每一步都做出一个局部最优的选择,最终的结果就是全局最优
- 这是一种特殊性质,其实只有一小部分问题拥有这个性质。
- 打牌不符合贪心,符合动规
6.1 区间调度问题⭐
详情
问题
- 给你很多形如[start,end]的闭区间,请你设计一个算法,算出这些区间中最多有几个互不相交的区间。
- 个例子,intvs=[[1,3],[2,4],[3,6]],这些区间最多有两个区间互不相交,即[[1,3],[3,6]],你的算法应该返回 2。注意边界相同并不算相交。
分析
- 使用贪心思想,若使区间的个数最多,应该使每个不相交的子区间长度更短,
- 长度由start点和end点决定,start的越晚,end越早的长度就会越短。
- 如果选择start晚的不能能保证一些start早点,end早点的更短区间不被忽略
- 所以正确的贪心思想是:每次选择最早的不相交end,则保证了每次选择的区间是最小的,区间的总数就是最多的。
代码
6.1.1 应用一:无重叠区间
问题
分析:
- 求出最多有几个区间不会重叠,那么剩下的就是至少需要去除的区间。
int eraseOverlapIntervals(int[][] intervals) {
int n = intervals.length;
return n - intervalSchedule(intervals);
}
6.1.2 最少的箭射气球
问题
分析
- 也是算不重叠的区间,几个不重叠的区间就需要几只箭。
- 以前是不同区间的边界可以重叠,现在重叠边接算到一个区间内。
- 变化就是找下一个区间时候的判断条件
代码
int findMinArrowShots(int[][] intvs) {
// ...
if(intvs.length == 0) return 0;
Arrays.sort(intvs,new Comparator<int[]>(){
@Override
public int Compare(int[] a,int[] b){
return a[1]-b[1]; }
});
int count = 1;
int x_end = intvs[0][1];
for (int[] interval : intvs) {
int start = interval[0];
// 把 >= 改成 > 就行了
if (start > x_end) {
count++;
x_end = interval[1];
}
}
return count;
}
6.1.3 跳跃游戏
7博弈问题
8 DP-路径问题
- 第一讲:62.不同路径(中等)
- 第二讲:63.不同路径 II(中等)
- 第三讲:64.最小路径和(中等)
- 第四讲:120.三角形最小路径和(中等)
- 第五讲:931.下降路径最小和(中等)
- 第六讲:1289.下降路径最小和 II(困难)
- 第七讲:1575.统计所有可行路径(困难)【记忆化搜索】
- 第八讲:1575.统计所有可行路径(困难)【动态规划】
- 第九讲:576.出界的路径数(中等)
- 第十讲:1301.最大得分的路径数目(困难)