1. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
- DP 状态:
dp[i]
表示爬到第i
阶楼梯的方法数。 - 状态转移方程: 爬到第
i
阶,可以从i-1
阶爬 1 步上来,也可以从i-2
阶爬 2 步上来。所以dp[i] = dp[i-1] + dp[i-2]
。 - 初始状态:
dp[0] = 1
(到达第 0 阶,视为一种方法),dp[1] = 1
(到达第 1 阶,只有一种方法)。 - 遍历顺序: 从
i = 2
到n
递增。
2. 最长公共子序列(Longest Common Subsequence, LCS)
给定两个字符串 S1 和 S2,找到这两个字符串的最长公共子序列的长度。子序列不要求连续。
- DP 状态:
dp[i][j]
表示字符串 S1 的前 i 个字符和字符串 S2 的前 j 个字符的最长公共子序列的长度。 - 状态转移方程:
- 如果 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])
(取两者中较大的)。
- 如果 S1[i−1]==S2[j−1] (当前字符相等),那么
- 初始状态:
dp[0][j] = 0
(S1 为空),dp[i][0] = 0
(S2 为空)。 - 遍历顺序: 外层循环 i 从 1 到 len(S1),内层循环 j 从 1 到 len(S2)。
3. 0-1 背包问题
特点: 每件物品只有一件,你只能选择放或不放(0 或 1)。
场景: 你要打包旅行,每件衣服只有一件,你决定带哪几件。
动态规划解法:
DP 状态:
dp[i][j]
表示在前i
件物品中选择,背包容量为j
时,能获得的最大价值。状态转移方程: 考虑第
i
件物品(假设其重量为w[i-1]
,价值为v[i-1]
):- 不选择第
i
件物品: 此时最大价值等于在前i-1
件物品中选择,背包容量为j
时的最大价值,即dp[i-1][j]
。 - 选择第
i
件物品: 前提是背包容量j
足够放下第i
件物品(即j >= w[i-1]
)。此时,剩余容量为j - w[i-1]
,我们需要在前i-1
件物品中选择,获得dp[i-1][j - w[i-1]]
的价值,再加上第i
件物品的价值v[i-1]
。所以是dp[i-1][j - w[i-1]] + v[i-1]
。
综上,当
j < w[i-1]
时,dp[i][j] = dp[i-1][j]
。 当j >= w[i-1]
时,dp[i][j] = max(dp[i-1][j], dp[i-1][j - w[i-1]] + v[i-1])
。- 不选择第
初始状态:
dp[0][j] = 0
(没有物品时,价值为0),dp[i][0] = 0
(背包容量为0时,价值为0)。遍历顺序: 外层循环物品
i
(从 1 到N
),内层循环背包容量j
(从 1 到W
)。
空间优化(一维数组):
由于 dp[i][j]
只依赖于 dp[i-1][...]
,我们可以将二维 dp
数组优化为一维 dp
数组。 dp[j]
此时表示背包容量为 j
时的最大价值。
- 状态转移:
dp[j] = max(dp[j], dp[j - w[i]] + v[i])
- 遍历顺序: 关键在于内层循环
j
必须从W
到w[i]
逆序遍历。 这是为了确保在计算dp[j]
时,dp[j - w[i]]
还是上一层(即不包含当前物品i
)的值。如果正序遍历,dp[j - w[i]]
可能已经被当前物品i
更新过,导致物品i
被重复选择。
Python
1 | def knapsack_01(weights, values, capacity): |
4. 完全背包问题
特点: 每件物品有无限多件,你可以选择任意件数放入背包。
场景: 你去超市购物,每种商品想买多少就买多少(只要钱够,购物车放得下)。
动态规划解法:
与 0-1 背包的关键区别在于,选择当前物品后,它仍然可以再次被选择。
- DP 状态:
dp[j]
表示背包容量为j
时,能获得的最大价值。 - 状态转移方程:
dp[j] = max(dp[j], dp[j - w[i]] + v[i])
- 遍历顺序: 与 0-1 背包不同的是,内层循环
j
必须从w[i]
到W
正序遍历。 这是因为在计算dp[j]
时,如果j - w[i]
已经包含了当前物品i
的选择,那么我们就可以继续选择物品i
。正序遍历确保了dp[j - w[i]]
已经“考虑”过当前物品i
的选择。
Python
1 | def complete_knapsack(weights, values, capacity): |
5. 多重背包问题
特点: 每件物品有固定的数量限制(例如,第 i
种物品有 count[i]
件)。
场景: 你去批货,每种商品商家限量供应。
动态规划解法:
多重背包可以看作是 0-1 背包和完全背包的结合。最直接的思路是将其转化为 0-1 背包问题。
转化思路:
将每件物品 i
及其数量 count[i]
展开成 count[i]
个独立的 0-1 背包问题中的物品。 例如,物品 A 有 3 件,重量 w_A
,价值 v_A
。我们可以把它看作 3 个独立的物品:A1 (w_A
, v_A
), A2 (w_A
, v_A
), A3 (w_A
, v_A
)。然后应用 0-1 背包的解法。
优化:二进制拆分
如果物品数量非常大,上述直接展开的方法会导致物品总数剧增,降低效率。更高效的方法是二进制拆分。 将每件物品 i
的数量 count[i]
拆分成若干个幂次为 2 的物品组合。 例如,如果 count[i] = 13
,我们可以拆分成:
- 1 件物品 (价值
1 * v[i]
, 重量1 * w[i]
) - 2 件物品 (价值
2 * v[i]
, 重量2 * w[i]
) - 4 件物品 (价值
4 * v[i]
, 重量4 * w[i]
) - 8 件物品 (价值
8 * v[i]
, 重量8 * w[i]
) – 1 + 2 + 4 + 8 = 15 > 13,所以最后一个是13 - (1+2+4) = 6
件物品 (价值6 * v[i]
, 重量6 * w[i]
)。
通过这种方式,任何 count[i]
件物品的选择都可以由这些二进制组合的物品来表示,而物品的总数大大减少。然后,对这些“新”物品应用 0-1 背包的解法。
Python
1 | def multiple_knapsack(weights, values, counts, capacity): |