动态规划算法:背包问题与最长公共子序列
引言:动态规划的核心思想
动态规划(Dynamic Programming, DP)是一种解决复杂问题的算法思想,通过将问题分解为子问题,并保存子问题的解,避免重复计算,从而提高效率。本文将详细讲解动态规划在 背包问题 和 最长公共子序列 中的应用,并提供易于记忆的代码模板。
一、背包问题
1.1 问题描述
给定 n
个物品,每个物品有一个重量 w[i]
和一个价值 v[i]
。现在有一个容量为 C
的背包,如何选择物品放入背包,使得背包中的物品总价值最大?
示例:
- 输入:
w = [2, 3, 4, 5]
,v = [3, 4, 5, 6]
,C = 8
- 输出:
10
- 解释:选择物品 1 和物品 3,总重量为
2 + 5 = 7
,总价值为3 + 6 = 9
。
1.2 动态规划思路
1.2.1 状态定义
dp[i][j]
:表示前i
个物品在容量为j
的背包中能获得的最大价值。
1.2.2 状态转移方程
- 如果不选择第
i
个物品:dp[i][j] = dp[i - 1][j]
- 如果选择第
i
个物品:dp[i][j] = dp[i - 1][j - w[i]] + v[i]
- 最终状态转移方程:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i])
1.2.3 初始化
dp[0][j] = 0
:没有物品时,价值为 0。dp[i][0] = 0
:背包容量为 0 时,价值为 0。
1.3 代码实现
1.3.1 Java 实现
public int knapsack(int[] w, int[] v, int C) {
int n = w.length;
int[][] dp = new int[n + 1][C + 1];
// 初始化
for (int i = 0; i <= n; i++) {
dp[i][0] = 0;
}
for (int j = 0; j <= C; j++) {
dp[0][j] = 0;
}
// 动态规划
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= C; j++) {
if (j >= w[i - 1]) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i - 1]] + v[i - 1]);
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n][C];
}
1.3.2 代码讲解
-
初始化:
dp[i][0] = 0
:背包容量为 0 时,价值为 0。dp[0][j] = 0
:没有物品时,价值为 0。
-
动态规划:
- 遍历每个物品
i
和每种容量j
。 - 如果当前容量
j
可以容纳物品i
,则选择是否放入物品i
。 - 更新
dp[i][j]
为最大值。
- 遍历每个物品
-
返回结果:
dp[n][C]
即为最大价值。
二、最长公共子序列(LCS)
2.1 问题描述
给定两个字符串 s1
和 s2
,找到它们的最长公共子序列的长度。
示例:
- 输入:
s1 = "abcde"
,s2 = "ace"
- 输出:
3
- 解释:最长公共子序列是
"ace"
,长度为 3。
2.2 动态规划思路
2.2.1 状态定义
dp[i][j]
:表示s1
的前i
个字符和s2
的前j
个字符的最长公共子序列长度。
2.2.2 状态转移方程
- 如果
s1[i - 1] == s2[j - 1]
:dp[i][j] = dp[i - 1][j - 1] + 1
- 如果
s1[i - 1] != s2[j - 1]
:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
2.2.3 初始化
dp[0][j] = 0
:s1
为空时,LCS 长度为 0。dp[i][0] = 0
:s2
为空时,LCS 长度为 0。
2.3 代码实现
2.3.1 Java 实现
public int longestCommonSubsequence(String s1, String s2) {
int m = s1.length(), n = s2.length();
int[][] dp = new int[m + 1][n + 1];
// 动态规划
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
2.3.2 代码讲解
-
初始化:
dp[0][j] = 0
:s1
为空时,LCS 长度为 0。dp[i][0] = 0
:s2
为空时,LCS 长度为 0。
-
动态规划:
- 遍历
s1
和s2
的每个字符。 - 如果字符相等,则
dp[i][j] = dp[i - 1][j - 1] + 1
。 - 如果字符不等,则
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
。
- 遍历
-
返回结果:
dp[m][n]
即为最长公共子序列的长度。
三、总结
动态规划是解决复杂问题的强大工具,通过将问题分解为子问题并保存子问题的解,可以显著提高效率。本文详细讲解了动态规划在 背包问题 和 最长公共子序列 中的应用,并提供了易于记忆的代码模板。
3.1 背包问题
- 状态定义:
dp[i][j]
表示前i
个物品在容量为j
的背包中的最大价值。 - 状态转移:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i])
。
3.2 最长公共子序列
- 状态定义:
dp[i][j]
表示s1
的前i
个字符和s2
的前j
个字符的最长公共子序列长度。 - 状态转移:
dp[i][j] = dp[i - 1][j - 1] + 1
(字符相等)或dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
(字符不等)。
通过掌握动态规划的通用模板,可以轻松应对类似的复杂问题。希望本文的讲解和代码实现能帮助大家更好地理解和记忆动态规划算法!