用填充表格法-理解01背包及其变体问题
动态规划(Dynamic Programming, DP)解决问题的核心逻辑,本质是通过填充表格 逐步推导最优解------把复杂的多阶段决策问题,转化为按规则填充表格的可视化过程。以01背包问题(最经典的DP模型)为例,我们先明确最终要填充的核心表格形态,后续所有解题步骤都是为了按规则完成这张表格,表格填完之时,就是问题解决之日。(看此文前,如果动态规划零基础,请看下DP解题的5步「钥匙」)
01背包问题核心表格(空表,后续逐步填充):
| 前i个物品\背包容量j | 0(容量为0) | 1(容量为1) | 2(容量为2) | ...(容量递增) | C(背包最大容量) |
|---|---|---|---|---|---|
| 0(无物品) | 待填充 | 待填充 | 待填充 | 待填充 | 待填充 |
| 1(第1个物品) | 待填充 | 待填充 | 待填充 | 待填充 | 待填充 |
| 2(第2个物品) | 待填充 | 待填充 | 待填充 | 待填充 | 待填充 |
| ...(物品递增) | 待填充 | 待填充 | 待填充 | 待填充 | 待填充 |
| n(第n个物品) | 待填充 | 待填充 | 待填充 | 待填充 | 待填充(最终答案) |
表格说明:表格中每个单元格dp[i][j]代表「前i个物品放入容量为j的背包的最大价值」,我们的目标就是按规则填充所有单元格,最终右下角dp[n][C]即为01背包问题的最优解。
要有序、正确地填充这张表格,需要遵循DP解题的5步「钥匙」------这是贯穿所有DP问题的通用拆解思路,每一步都对应表格填充的关键环节:
-
确定dp数组及下标的含义 :明确
dp[i](或二维dp[i][j])代表什么物理意义(比如"第i阶台阶的爬法数"、"前i个物品放入容量为j的背包的最大价值"); -
确定递推公式 :找到
dp[i]与子问题dp[i-1]/dp[i-2]等的依赖关系(这是DP的核心,本质是将大问题拆解为可复用的子问题); -
dp数组如何初始化:根据问题边界条件,初始化无法通过递推得到的基础值(比如"背包容量为0时价值为0"、"无物品时价值为0");
-
确定遍历顺序 :保证计算
dp[i]时,其依赖的子问题已经被计算完成。这里可以简单理解为「表格的填充顺序」------ 二维DP类似逐行逐列填充表格,一维DP则是按特定方向(正序/倒序)填充单行表格; -
打印dp数组(验证):通过打印中间结果,验证递推逻辑是否正确(这是调试DP问题的必备步骤,能快速定位递推公式或遍历顺序的错误)。
后续所有01背包及变体问题的分析,都将围绕这5步「钥匙」展开,本质就是用这5步规则完成对应表格的填充,最终通过表格得到问题答案。
一、01背包问题基础定义
01背包的核心场景:有n个物品,每个物品只有1个 (要么选、要么不选,即"0-1"选择),每个物品有对应的重量w[i]和价值v[i],现有一个容量为C的背包,如何选择物品放入背包,使得放入物品的总价值最大?
示例:物品重量weights = [2,3,4,5],价值values = [3,4,5,6],背包容量capacity = 8,求最大价值。
二、基础解法:二维DP数组
我们将严格按照DP解题的5步「钥匙」拆解二维DP解法,每一步都对应核心表格的填充规则:
2.1 步骤1:确定dp数组及下标的含义
定义二维DP数组dp[i][j]:表示「前i个物品(从第1个到第i个),放入容量为j的背包中,能获得的最大价值」。
说明:
-
「前
i个物品」不是指编号为i的物品,而是范围上的前i个(比如i=2代表前2个物品); -
下标
i的范围:0n(C(i=0表示无物品),下标j的范围:0j=0表示背包容量为0)。
2.2 步骤2:确定递推公式
对于第i个物品(重量w[i-1]、价值v[i-1],因数组下标从0开始),有两种选择:选或不选,递推公式由此而来:
-
i不选第个物品 :前i个物品的最大价值 = 前i-1个物品的最大价值,即dp[i][j] = dp[i-1][j]; -
i选第个物品 :需保证背包容量j ≥ w[i-1],此时价值 = 前i-1个物品放入容量j - w[i-1]的最大价值 + 第i个物品的价值,即dp[i][j] = dp[i-1][j - w[i-1]] + v[i-1]。
综合两种情况,取最大值作为dp[i][j]的结果:
Plain
dp[i][j] = max(dp[i-1][j], 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]时,容量不足,无法选第i个物品)
2.3 步骤3:dp数组如何初始化
根据边界条件初始化:
-
i=0(无物品):无论背包容量多大,最大价值都是0,即dp[0][j] = 0(j从0到C); -
j=0(背包容量为0):无论有多少物品,都无法放入,最大价值都是0,即dp[i][0] = 0(i从0到n)。
初始化后,DP数组的第一行和第一列都为0。
2.4 步骤4:确定遍历顺序(表格填充顺序)
遍历顺序直接对应二维表格的填充顺序------即「按什么顺序逐个填写表格中的单元格」,这里有两种可行方式:
-
先遍历物品(
i从1到n),再遍历背包容量(j从1到C); -
先遍历背包容量(
j从1到C),再遍历物品(i从1到n)。
两种顺序都可行,因为计算dp[i][j]时,只依赖上一行(i-1行)的结果,无论先填行还是先填列,上一行的对应位置都已提前计算完成。这就像填充一张二维表格:先遍历物品再遍历容量,是逐行填充 (每一行对应一个物品的决策,填完一行再处理下一个物品);先遍历容量再遍历物品,是逐列填充(每一列对应一个固定容量,先确定所有物品在该容量下的最优解)。实际解题中更常用「先遍历物品,再遍历容量」的顺序,符合我们「逐个考虑物品是否放入」的思考逻辑。
2.5 步骤5:打印dp数组(验证)
这一步是直接验证表格填充结果的正确性------通过逐步填充表格、打印中间状态,确认每一步都符合递推规则,避免因规则理解偏差导致填充错误。以示例weights = [2,3,4,5]、values = [3,4,5,6]、capacity = 8为例,逐步填充核心表格验证逻辑:
以示例weights = [2,3,4,5]、values = [3,4,5,6]、capacity = 8为例,逐步填充表格验证逻辑:
初始DP数组(第一行、第一列为0):
| 前i个物品\背包容量j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| 0(无物品) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
步骤1:处理第1个物品(重量2,价值3)
j≥2时取max(上一行j, 上一行j-2+3):
| 前i个物品\背包容量j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| 1(物品[2,3]) | 0 | 0 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
步骤2:处理第2个物品(重量3,价值4)
j≥3时取max(上一行j, 上一行j-3+4):
| 前i个物品\背包容量j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| 2(物品[2,3]、[3,4]) | 0 | 0 | 3 | 4 | 4 | 7 | 7 | 7 | 7 |
步骤3:处理第3个物品(重量4,价值5)
j≥4时取max(上一行j, 上一行j-4+5):
| 前i个物品\背包容量j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| 3(新增[4,5]) | 0 | 0 | 3 | 4 | 5 | 7 | 8 | 9 | 9 |
步骤4:处理第4个物品(重量5,价值6)
j≥5时取max(上一行j, 上一行j-5+6):
| 前i个物品\背包容量j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| 4(新增[5,6]) | 0 | 0 | 3 | 4 | 5 | 7 | 8 | 9 | 10 |
最终dp[4][8] = 10,与测试用例结果一致,即最大价值为10。
完整二维DP数组变化汇总表:
| 前i个物品\背包容量j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| 0(无物品) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1(物品[2,3]) | 0 | 0 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
| 2(物品[2,3]、[3,4]) | 0 | 0 | 3 | 4 | 4 | 7 | 7 | 7 | 7 |
| 3(新增[4,5]) | 0 | 0 | 3 | 4 | 5 | 7 | 8 | 9 | 9 |
| 4(新增[5,6]) | 0 | 0 | 3 | 4 | 5 | 7 | 8 | 9 | 10 |
-
时间复杂度:
O(n*C)(n为物品数,C为背包容量); -
空间复杂度:
O(n*C)(二维数组的空间开销)。