动态规划讲解例题:
补充练习题:力扣上经典的四个动态规划系列题
(以下正片开始)
第3章 动态规划(续)
回顾总结:动态规划三大特性
- 最优子结构
- 当一个最优解包含子结构的解时,子结构的解也必定是最优解
- 一个问题可以使用动态规划的前提条件
- 证明该问题可用动态规划求解
- 重叠子问题
- 使用动态规划的必要性
- 解释了动态规划为什么能优化算法
- 备忘录方法
- 求解重叠子问题的工具
- 使用动态规划解题的特征
例题4 LC300 最长递增子序列
本题为补充自学内容,要点提示如下:
- 注意 d p [ i ] dp[i] dp[i] 的定义:在 n u m s [ 0.. i ] nums[0..i] nums[0..i] 中,并且以 n u m s [ i ] nums[i] nums[i] 为结尾的最长递增子序列的长度
- 在求解 d p [ i ] dp[i] dp[i] 的过程中必须枚举 前面[0…i-1]的子问题结果,首先必须 n u m s [ j ] < n u m s [ i ] nums[j] < nums[i] nums[j]<nums[i],然后计算得到的 d p [ i ] = d p [ j ] + 1 dp[i]=dp[j] + 1 dp[i]=dp[j]+1是否更长
- 时间复杂度为 O ( n O(n O(n2 ) ) )
Java 代码实现
提交记录原链接 (登录你自己的力扣账号之后可见)
class Solution {
public int lengthOfLIS(int[] a) {
int n = a.length, max_len = 1; // max_len: 最终结果
int[] dp = new int[n];
for (int i = 0; i < n; i++) {
dp[i] = 1; // 至少为1
for (int j = 0; j < i; j++) // 枚举前面的结果
if (a[j] < a[i]) { // 前提:前一个数和当前为递增
dp[i] = Math.max(dp[i], dp[j] + 1); // 递推
max_len = Math.max(max_len, dp[i]);
}
}
return max_len;
}
}
Python代码实现
提交记录原链接 (登录你自己的力扣账号之后可见)
强烈建议阅读Python代码,Python简洁的语法能够更直观地体现算法思想
class Solution:
def lengthOfLIS(self, a: List[int]) -> int:
n = len(a)
dp = [0] * n
for i in range(n):
# 注意这里必须加上default参数,因为当i前面没有更小的元素时,列表生成式为空,此时无法求得max
dp[i] = 1 + max((dp[j] for j in range(i) if a[j] < a[i]), default = 0)
return max(dp)
例题5 LC1143 最长公共子序列
问题分析
1. 最优子结构
我们假设问题的最优解 c c c,由两个子串 a a a和 b b b组成(即 c = a + b c=a+b c=a+b),又知 a a a是 s 1 ′ s1' s1′和 s 2 ′ s2' s2′的公共子序列,那我们必定有 a a a是 s 1 ′ s1' s1′和 s 2 ′ s2' s2′的最长公共子序列
2. 重叠子问题
例如我们求解 ( s 1 [ 4 ] , s 2 [ 6 ] ) (s1[4], s2[6]) (s1[4],s2[6]),和 ( s 1 [ 5 ] , s 2 [ 4 ] ) (s1[5], s2[4]) (s1[5],s2[4])这两个问题时,都会遇到 ( s 1 [ 3 ] , s 2 [ 3 ] ) (s1[3], s2[3]) (s1[3],s2[3])这个子问题,如果每次都计算一遍就会造成大量重复计算
3. 备忘录方法
我们用二维数组(备忘录)dp[a][b]
表示
s
1
s1
s1的前
a
a
a个字母与
s
2
s2
s2的前
b
b
b个字母组成的最长公共子序列的长度,避免重叠子问题的重复计算
求解过程
设 dp[a][b]
表示
s
1
s1
s1的前
a
a
a个字母与
s
2
s2
s2的前
b
b
b个字母组成的最长公共子序列的长度
dp[a][b]
分为以下三种状态:
s1[a-1]
为多出来的字母,所以答案和dp[a-1][b]
一样s2[b-1]
为多出来的字母,所以答案为dp[a][b-1]
- 当
s1[a-1]==s2[b-1]
时,两个字母构成长度为 1 1 1的子序列,再加上前面子问题的最优解,即1+dp[a-1][b-1]
dp[a][b]
取以上三种情况的最大值
当S1="abcd" S2="acedb"
时,填表情况如下图:
Java代码实现
class Solution {
int longestCommonSubsequence(String s1, String s2) {
int[][] dp = new int[s1.length()][s2.length()];
for (int i = 0; i < s1.length(); i++)
for (int j = 0; j < s2.length(); j++) {
// 求dp[i][j]: 必定为以下三个分支的最大值
// 分支1: 由dp[i - 1][j - 1] + 1得到当前值
if (s1.charAt(i) == s2.charAt(j)) {
dp[i][j] = 1;
if (i > 0 && j > 0) dp[i][j] += dp[i - 1][j - 1];
}
// 分支2: s1[i]是多出来的字符,当前值跟dp[i - 1][j]相等
if (i > 0 && dp[i - 1][j] > dp[i][j]) dp[i][j] = dp[i - 1][j];
// 分支3: s2[j]是多出来的字符,当前值跟dp[i][j - 1]相等
if (j > 0 && dp[i][j - 1] > dp[i][j]) dp[i][j] = dp[i][j - 1];
}
// 返回递推表的最后一个值为两个字串的最长公共子序列的长度
return dp[s1.length() - 1][s2.length() - 1];
}
}
Python代码实现
class Solution:
# 求最长公共子序列
def longestCommonSubsequence(self, s1: str, s2: str) -> int:
m, n = len(s1), len(s2)
# 创建备忘录,求解重叠子问题
# 令dp[i][j] 为 s1[0..i-1](也就是前i个字符)
# 与 s2[0..j-1](也就是前j个字符)的LCS长度
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
# 分支1:两个子串的末尾字符相等
if s1[i - 1] == s2[j - 1]:
dp[i][j] = 1 + dp[i - 1][j - 1]
# 分支2:末尾字符不相等
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
# 返回结果
return dp[-1][-1]
例题6 01背包问题
本题为自主学习内容,提示如下:
- 01背包问题为NP难度问题,在背包容量可被枚举的情况下可用动态规划求解
- 01背包问题的时间复杂度为O(nC) (考试必考点)
- 定义
dp[i][c]
:把前 i i i个物品装进容量为 c c c的子背包可获得的最大价值 - 选择状态:第
i
i
i件物品不装背包,价值和
i
−
1
i-1
i−1一样,或者装背包得到
v[i]
,然后查找剩余容量剩余物品下的最大价值dp[i-1][c-w[i]]
- 递推公式:
dp[i][c] = max(dp[i-1][c], v[i]+dp[i-1][c-w[i]])
示例填表如下:
示例代码如下:
以下代码已优化,可作为代码模板保存,遇到0-1背包问题时可以直接套用
/**
* 01背包问题:计算可装入背包物品的最大价值总和
* @param n 物品的数量
* @param c 背包的最大承重量
* @param w 物品重量 w[i]对应第i件物品的重量
* @param v 物品价值 v[i]对应第i件物品的价值
* @return 返回可装入背包物品的最大价值
*/
public int knapsack(int n, int c, int[] w, int[] v) {
int[] dp = new int[c + 1];
for (int i = 0; i < n; i++)
for (int j = c; j >= w[i]; j--)
dp[j] = Math.max( dp[j], v[i] + dp[j - w[i]]);
return dp[c];
}