01背包问题
问题描述:
给定一组物品,每种物品有一个重量 weight[i] 和一个价值 value[i]。有一个容量为 capacity的背包,要求选择一些物品放入背包中,使得背包中物品的总重量不超过 capacity,且总价值最大。每种物品只能选择一次(要么选,要么不选)。
二维 dp 数组解法
1. dp 数组的含义
设 dp[i][j]
表示:前 i
个物品在容量为 j
的背包中能够获得的最大价值。
2. dp 数组的初始化
dp[i][0] = 0
(背包容量为 0 时,无论选多少物品,价值都为 0)。dp[0][j] = 0
(没有物品时,价值为 0)。
3. 递推公式
对于每个物品 i
,两种情况:
-
不选
i
号物品:dp[i][j] = dp[i-1][j]
-
选
i
号物品(前提是j >= weight[i]
):d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[i][j]=max(dp[i−1][j],dp[i−1][j−weight[i]]+value[i]) dp[i][j]=max(dp[i−1][j],dp[i−1][j−weight[i]]+value[i])
4. 遍历顺序
先遍历物品、先遍历背包容量都可以
for (int i = 1; i <= n; i++) { // 遍历物品
for (int j = 0; j <= W; j++) { // 遍历背包容量(正序遍历)
if (j >= weight[i])
dp[i][j] = max(dp[i-1][j], dp[i-1][j - weight[i]] + value[i]);
else
dp[i][j] = dp[i-1][j]; // 不能选物品 i,则价值不变
}
}
一维 dp 数组优化
1. dp 数组的含义
设 dp[j]
表示:容量为 j
的背包能获得的最大价值(在遍历到某个物品 i
时的状态)。
2. dp 数组的初始化
dp[0] = 0
(背包容量为 0 时,价值为 0)。- 其余
dp[j]
也初始化为 0(表示初始状态下没有选择任何物品)。
3. 递推公式
d p [ j ] = m a x ( d p [ j ] , d p [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[j]=max(dp[j],dp[j−weight[i]]+value[i]) dp[j]=max(dp[j],dp[j−weight[i]]+value[i])
为什么可以省略 dp[i-1][j]
?
- 因为
dp[j]
依赖于dp[j - weight[i]]
,若从小到大遍历j
,会覆盖dp[j - weight[i]]
的旧值。 - 所以
j
必须倒序遍历,保证dp[j - weight[i]]
在本轮计算前仍然存储的是上一轮i-1
时的值。
4. 遍历顺序(注意倒序遍历容量)
for (int i = 0; i < n; i++) { // 遍历物品
for (int j = W; j >= weight[i]; j--) { // 遍历背包容量(倒序)
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
三、总结
二维 dp 数组 | 一维 dp 数组(空间优化) | |
---|---|---|
dp 含义 | dp[i][j] :前 i 个物品、背包容量 j 时的最大价值 | dp[j] :容量 j 时的最大价值 |
初始化 | dp[i][0] = 0 ,dp[0][j] = 0 | dp[0] = 0 ,其余 dp[j] = 0 |
递推公式 | dp[i][j] = max(dp[i-1][j], dp[i-1][j - weight[i]] + value[i]) | dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) |
遍历顺序 | 物品在外层,容量 j 正序遍历 | 物品在外层,容量 j 倒序遍历 |
空间复杂度 | O(nW) | O(W) |
416.分割等和子集
思路:01背包问题
给定一个数组 nums
,判断是否能将其拆分为两个和相等的子集。等价于在 nums
中找到一个子集,其总和为 sum / 2
。
方法一:基于最大价值的 DP
-
定义
dp[j]
:表示容量j
的背包能获得的最大价值。 -
目标:如果
dp[target] == target
,说明可以凑成sum/2
,即可以拆分成功。 -
递推公式:
d p [ j ] = max ( d p [ j ] , d p [ j − n u m s [ i ] ] + n u m s [ i ] ) dp[j] = \max(dp[j], dp[j - nums[i]] + nums[i]) dp[j]=max(dp[j],dp[j−nums[i]]+nums[i])- 不选
nums[i]
:dp[j]
维持不变。 - 选
nums[i]
:dp[j]
变成dp[j - nums[i]] + nums[i]
。
- 不选
-
遍历顺序:
- 外层 遍历物品
i
。 - 内层 倒序 遍历
j
,防止覆盖dp[j - nums[i]]
。
- 外层 遍历物品
代码一(一维dp)
#include <stdbool.h>
#include <string.h>
bool canPartition(int* nums, int numsSize) {
int sum = 0;
for (int i = 0; i < numsSize; i++) sum += nums[i];
if (sum % 2 != 0) return false;
int target = sum / 2;
int dp[target + 1];
memset(dp, 0, sizeof(dp)); // 初始化为 0
for (int i = 0; i < numsSize; i++) { // 遍历物品
for (int j = target; j >= nums[i]; j--) { // **倒序遍历**
dp[j] = fmax(dp[j], dp[j - nums[i]] + nums[i]);
}
}
return dp[target] == target;
}
代码二(二维dp)
#include <stdbool.h>
#include <string.h>
#include <stdio.h>
bool canPartition(int* nums, int numsSize) {
int sum = 0;
for (int i = 0; i < numsSize; i++) sum += nums[i];
if (sum % 2 != 0) return false; // 如果总和是奇数,无法拆分
int target = sum / 2;
int dp[numsSize + 1][target + 1];
memset(dp, 0, sizeof(dp)); // 初始化所有值为 0
for (int i = 1; i <= numsSize; i++) {
for (int j = 0; j <= target; j++) {
if (j >= nums[i - 1]) {
dp[i][j] = fmax(dp[i-1][j], dp[i-1][j - nums[i - 1]] + nums[i - 1]);
} else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[numsSize][target] == target;
}
方法二:基于可行性的布尔 DP
-
定义
dp[j]
:表示是否存在一个子集,其和恰好为j
。 -
目标:如果
dp[target] == true
,说明可以拆分成功。 -
递推公式:
d p [ j ] = d p [ j ] ∨ d p [ j − n u m s [ i ] ] dp[j]=dp[j]∨dp[j−nums[i]] dp[j]=dp[j]∨dp[j−nums[i]]- 不选
nums[i]
:dp[j]
维持不变。 - 选
nums[i]
(前提j >= nums[i]
):dp[j] = dp[j] || dp[j - nums[i]]
。
- 不选
-
遍历顺序:
- 外层 遍历物品
i
。 - 内层 倒序 遍历
j
,防止覆盖dp[j - nums[i]]
。
- 外层 遍历物品
代码一(一维dp)
#include <stdbool.h>
#include <string.h>
bool canPartition(int* nums, int numsSize) {
int sum = 0;
for (int i = 0; i < numsSize; i++) sum += nums[i];
if (sum % 2 != 0) return false;
int target = sum / 2;
bool dp[target + 1];
memset(dp, false, sizeof(dp));
dp[0] = true; // 和为 0 时一定可行
for (int i = 0; i < numsSize; i++) {
for (int j = target; j >= nums[i]; j--) { // **倒序遍历**
dp[j] = dp[j] || dp[j - nums[i]];
}
}
return dp[target];
}
代码二(二维dp)
#include <stdbool.h>
#include <string.h>
#include <stdio.h>
bool canPartition(int* nums, int numsSize) {
int sum = 0;
for (int i = 0; i < numsSize; i++) sum += nums[i];
if (sum % 2 != 0) return false; // 总和是奇数,不能拆分
int target = sum / 2;
bool dp[numsSize + 1][target + 1];
memset(dp, false, sizeof(dp)); // 初始化所有值为 false
for (int i = 0; i <= numsSize; i++) dp[i][0] = true; // 和为 0 时一定可行
for (int i = 1; i <= numsSize; i++) {
for (int j = 1; j <= target; j++) {
if (j >= nums[i - 1]) {
dp[i][j] = dp[i-1][j] || dp[i-1][j - nums[i - 1]];
} else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[numsSize][target];
}
1049.最后一块石头的重量Ⅱ
思路:01背包问题
1. DP 数组含义
dp[j]
表示容量为j
的背包能装的最大重量。- 在这个问题中,石头的重量既是物品的重量,也是物品的价值。
- 我们需要找到一个子集,使得它们的总重量尽可能接近
target = sum / 2
。
2. DP 数组初始化
dp[0] = 0
:表示背包容量为 0 时,能装的最大重量为 0。- 其他
dp[j]
初始化为 0,表示初始状态下,背包容量为j
时,能装的最大重量为 0。
3. DP 状态转移方程
-
对于每个石头
stones[i]
,我们有两种选择:- 不选当前石头:
dp[j]
保持不变。 - 选当前石头:
dp[j - stones[i]] + stones[i]
。
- 不选当前石头:
-
状态转移方程为:
dp[j] = fmax(dp[j], dp[j - stones[i]] + stones[i]);
4. DP 遍历顺序
- 外层循环:遍历每个石头(物品),从
0
到stonesSize - 1
。 - 内层循环:倒序遍历背包容量,从
target
到stones[i]
。- 倒序遍历是为了确保每个石头只被使用一次(0-1 背包的特性)。
- 如果正序遍历,会导致同一个石头被重复使用(完全背包的特性)。
5. 返回值
- 最终的最小差值为
sum - 2 * dp[target]
。dp[target]
是容量为target
的背包能装的最大重量。- 另一堆石头的重量为
sum - dp[target]
。 - 两堆石头的重量差为
(sum - dp[target]) - dp[target] = sum - 2 * dp[target]
。
代码
int lastStoneWeightII(int* stones, int stonesSize) {
// 1. 计算所有石头的总重量
int sum = 0;
for (int i = 0; i < stonesSize; i++) {
sum += stones[i];
}
// 2. 确定目标值 target
// 将石头分成两堆,使得两堆的重量差最小
// 目标是将其中一堆的重量尽可能接近 sum / 2
int target = sum / 2;
// 3. DP 数组定义
// dp[j] 表示容量为 j 的背包能装的最大重量
// 这里将石头的重量同时视为物品的重量和价值
int dp[target + 1];
// 4. DP 数组初始化
// 初始化为 0,表示背包容量为 j 时,能装的最大重量为 0
memset(dp, 0, sizeof(dp));
// 5. 动态规划填充 DP 数组
// 外层循环遍历每个石头(物品)
for (int i = 0; i < stonesSize; i++) {
// 内层循环倒序遍历背包容量
// 从 target 到 stones[i],确保每个石头只被使用一次
for (int j = target; j >= stones[i]; j--) {
// 状态转移方程
// dp[j] 表示不选当前石头时的最大重量
// dp[j - stones[i]] + stones[i] 表示选当前石头时的最大重量
// 取两者的最大值
dp[j] = fmax(dp[j], dp[j - stones[i]] + stones[i]);
}
}
// 6. 返回值
// 最终的最小差值为总重量减去两堆石头的重量
// 其中一堆的重量为 dp[target],另一堆的重量为 sum - dp[target]
// 差值为 (sum - dp[target]) - dp[target] = sum - 2 * dp[target]
return sum - 2 * dp[target];
}
494.目标和
思路:01背包问题
1.DP 数组定义
dp[i][j]
表示前i
个元素中,和为j
的组合数。
2.DP 数组初始化
dp[0][0] = 1
:表示前 0 个元素中,和为 0 的组合数为 1。- 其他
dp[i][j]
初始化为 0。
3.状态转移:
- 对于每个元素
nums[i]
,更新dp[i][j]
:- 如果不选
nums[i]
,则dp[i][j] = dp[i-1][j]
。 - 如果选
nums[i]
,则dp[i][j] += dp[i-1][j - nums[i]]
。
- 如果不选
4.遍历顺序
- 外层循环遍历元素(
i
从 1 到numsSize
)。 - 内层循环遍历目标和(
j
从 0 到newTarget
)。 - 对于每个元素
nums[i-1]
,更新dp[i][j]
:- 如果不选
nums[i-1]
,则dp[i][j] = dp[i-1][j]
。 - 如果选
nums[i-1]
,则dp[i][j] += dp[i-1][j - nums[i-1]]
。
- 如果不选
5. 返回值
- 返回
dp[numsSize][newTarget]
,表示前numsSize
个元素中,和为newTarget
的组合数。
代码一:二维dp
int findTargetSumWays(int* nums, int numsSize, int target) {
// 1. 计算数组的总和
int sum = 0;
for (int i = 0; i < numsSize; i++) {
sum += nums[i];
}
// 2. 边界条件
// 如果 target 的绝对值大于 sum,直接返回 0
if (abs(target) > sum) return 0;
// 如果 (sum + target) 是奇数,直接返回 0
if ((sum + target) & 1) return 0;
// 3. 计算目标值
int newTarget = (sum + target) / 2;
// 4. DP 数组定义
// dp[i][j] 表示前 i 个元素中,和为 j 的组合数
int dp[numsSize + 1][newTarget + 1];
// 5. DP 数组初始化
for (int i = 0; i <= numsSize; i++) {
for (int j = 0; j <= newTarget; j++) {
dp[i][j] = 0;
}
}
dp[0][0] = 1; // 前 0 个元素中,和为 0 的组合数为 1
// 6. 动态规划填充 DP 数组
for (int i = 1; i <= numsSize; i++) {
for (int j = 0; j <= newTarget; j++) {
dp[i][j] = dp[i - 1][j]; // 不选 nums[i-1]
if (j >= nums[i - 1]) {
dp[i][j] += dp[i - 1][j - nums[i - 1]]; // 选 nums[i-1]
}
}
}
// 7. 返回值
return dp[numsSize][newTarget];
}
代码二:一维dp
DP 数组的含义:dp[j]
表示 选取若干个数,使其和恰好为 j
的方案数。
DP 数组初始化:dp[0] = 1
(表示选取空集的方案数为 1),其余 dp[j] = 0
(初始时和不可能是 j
)。
DP 状态转移方程:dp[j] = dp[j] + dp[j - nums[i]]
,即 不选或选当前元素 两种方案的总和。
DP 遍历顺序:先遍历 nums[i]
,再倒序遍历 j
,确保每个元素只能选一次,符合 0-1 背包特性。
返回值:dp[newTarget]
,即 选取若干个数,使其和为 newTarget
的方案数。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int findTargetSumWays(int* nums, int numsSize, int target) {
// 1. 计算数组总和
int sum = 0;
for (int i = 0; i < numsSize; i++) {
sum += nums[i];
}
// 2. 边界检查
if (abs(target) > sum) return 0; // target 不能超过 sum
if ((sum + target) & 1) return 0; // (sum + target) 必须为偶数
// 3. 计算背包容量
int newTarget = (sum + target) / 2;
// 4. 定义 DP 数组,并初始化
int dp[newTarget + 1];
memset(dp, 0, sizeof(dp));
dp[0] = 1; // 选取空集时,和为 0 的方案数为 1
// 5. 0-1 背包填充 DP 数组(倒序遍历)
for (int i = 0; i < numsSize; i++) {
for (int j = newTarget; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
// 6. 返回结果
return dp[newTarget];
}
474.一和零
思路:01背包问题
1. DP 数组含义
dp[i][j]
表示最多使用i
个0
和j
个1
时,可以选择的字符串的最大数量。- 这是一个二维动态规划问题,
i
表示0
的数量限制,j
表示1
的数量限制。
2. DP 数组初始化
dp[0][0] = 0
:表示不使用任何0
和1
时,可以选择的最大字符串数量为 0。- 其他
dp[i][j]
初始化为 0,表示初始状态下,最多使用i
个0
和j
个1
时,可以选择的最大字符串数量为 0。
3. DP 数组递推
-
对于每个字符串
strs[k]
,其0
的数量为num0[k]
,1
的数量为num1[k]
。 -
更新
dp[i][j]
的状态转移方程为:d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i − n u m 0 [ k ] ] [ j − n u m 1 [ k ] ] + 1 ) dp[i][j] = max(dp[i][j], dp[i - num0[k]][j - num1[k]] + 1) dp[i][j]=max(dp[i][j],dp[i−num0[k]][j−num1[k]]+1)
- 如果不选当前字符串,则
dp[i][j]
保持不变。 - 如果选当前字符串,则需要减去当前字符串的
0
和1
的数量,并加 1(表示选择当前字符串)。
- 如果不选当前字符串,则
4. DP 数组遍历
- 外层循环:遍历每个字符串(物品),从
0
到strsSize - 1
。 - 内层循环:倒序遍历
0
的数量(从m
到num0[k]
)。 - 最内层循环:倒序遍历
1
的数量(从n
到num1[k]
)。 - 倒序遍历确保每个字符串只被使用一次(0-1 背包的特性)。
5. 返回值
- 返回
dp[m][n]
,表示最多使用m
个0
和n
个1
时,可以选择的字符串的最大数量。
代码
int findMaxForm(char** strs, int strsSize, int m, int n) {
// 1. 统计每个字符串中 0 和 1 的数量
int num0[strsSize], num1[strsSize];
memset(num0, 0, sizeof(num0)); // 初始化 num0 数组为 0
memset(num1, 0, sizeof(num1)); // 初始化 num1 数组为 0
// 遍历每个字符串,统计其中 0 和 1 的数量
for (int i = 0; i < strsSize; i++) {
for (int j = 0; strs[i][j] != '\0'; j++) {
if (strs[i][j] == '0') num0[i]++; // 统计 0 的数量
else num1[i]++; // 统计 1 的数量
}
}
// 2. DP 数组定义
// dp[i][j] 表示最多使用 i 个 0 和 j 个 1 时,可以选择的字符串的最大数量
int dp[m + 1][n + 1];
memset(dp, 0, sizeof(dp)); // 初始化 dp 数组为 0
// 3. 动态规划填充 DP 数组
// 外层循环遍历每个字符串(物品)
for (int k = 0; k < strsSize; k++) {
// 内层循环倒序遍历 0 的数量(从 m 到 num0[k])
for (int i = m; i >= num0[k]; i--) {
// 最内层循环倒序遍历 1 的数量(从 n 到 num1[k])
for (int j = n; j >= num1[k]; j--) {
// 状态转移方程
// dp[i][j] 表示不选当前字符串时的最大数量
// dp[i - num0[k]][j - num1[k]] + 1 表示选当前字符串时的最大数量
// 取两者的最大值
dp[i][j] = fmax(dp[i][j], dp[i - num0[k]][j - num1[k]] + 1);
}
}
}
// 4. 返回值
// dp[m][n] 表示最多使用 m 个 0 和 n 个 1 时,可以选择的字符串的最大数量
return dp[m][n];
}
完全背包问题(与01进行比较)
给定一组物品,每种物品有一个重量 weight[i]和一个价值 value[i]。有一个容量为 capacity的背包,要求选择一些物品放入背包中,使得背包中物品的总重量不超过 capacity,且总价值最大。每种物品可以选择多次(无限次)。
1. dp数组含义
- 完全背包:
dp[j]
表示容量为j
的背包,能装下的最大价值。 - 01背包:
dp[j]
表示容量为j
的背包,能装下的最大价值。
两者的dp数组含义相同,都是表示在给定容量下的最大价值。
2. dp数组初始化
- 完全背包:
dp[0] = 0
,表示背包容量为0时,最大价值为0。其他位置可以初始化为0,表示未选择任何物品时的价值。 - 01背包:
dp[0] = 0
,表示背包容量为0时,最大价值为0。其他位置可以初始化为0,表示未选择任何物品时的价值。
两者的初始化方式相同。
3. dp数组递推公式
- 完全背包:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
,其中weight[i]
和value[i]
分别表示第i
种物品的重量和价值。 - 01背包:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
,其中weight[i]
和value[i]
分别表示第i
种物品的重量和价值。
两者的递推公式相同,但完全背包问题允许重复选择物品,因此在遍历时有所不同。
4. dp数组遍历顺序
- 完全背包:
- 外层循环:遍历物品,从第1个物品到第
n
个物品。 - 内层循环:遍历背包容量,从
weight[i]
到capacity
,从小到大遍历。 - 从小到大遍历的原因是允许重复选择物品,因此在更新
dp[j]
时,dp[j - weight[i]]
可能已经包含了当前物品的多次选择。 - 也可以先遍历背包,后遍历物品
- 外层循环:遍历物品,从第1个物品到第
- 01背包:
- 外层循环:遍历物品,从第1个物品到第
n
个物品。 - 内层循环:遍历背包容量,从
capacity
到weight[i]
,从大到小遍历。 - 从大到小遍历的原因是每个物品只能选择一次,因此在更新
dp[j]
时,dp[j - weight[i]]
不能包含当前物品的选择。
- 外层循环:遍历物品,从第1个物品到第
518.零钱兑换Ⅱ
思路:完全背包
1.问题转化:
- 将硬币视为物品,每种硬币可以无限次使用(完全背包问题)。
- 我们需要找到所有组合方式,使得硬币的总和等于
amount
。
2. DP 数组定义
dp[j]
表示凑成金额j
的组合数。
3. DP 数组初始化
dp[0] = 1
:表示凑成金额0
的组合数为 1(不选任何硬币)。- 其他
dp[j]
初始化为 0。
4. 动态规划填充 DP 数组
- 外层循环遍历硬币(物品)。
- 内层循环正序遍历金额(从
coins[i]
到amount
)。 - 对于每个硬币
coins[i]
,更新dp[j]
:dp[j] += dp[j - coins[i]]
。
5. 返回值
- 返回
dp[amount]
,表示凑成金额amount
的组合数。
代码
正常来说按照上面的思路写代码就可以通过了,但是测试用例中存在返回值为0,即无法凑齐的数据,但是在计算的中间值中存在大于int甚至long long的值。有下面两种方法:
1.在计算dp数组前先判断是否能凑齐
int change(int amount, int* coins, int coinsSize) {
// 1. DP 数组定义
// dp[j] 表示凑成金额 j 的组合数
int dp[amount + 1];
memset(dp, 0, sizeof(dp)); // 初始化为 0
// valid[j] 表示是否可以凑成金额 j
bool valid[amount + 1];
memset(valid, false, sizeof(valid)); // 初始化为 false
valid[0] = true; // 金额 0 可以被凑成
// 2. 检查是否可以凑成目标金额
for (int i = 0; i < coinsSize; i++) {
for (int j = coins[i]; j <= amount; j++) {
valid[j] = valid[j] | valid[j - coins[i]]; // 更新 valid 数组
}
}
// 如果无法凑成目标金额,直接返回 0
if (!valid[amount]) return 0;
// 3. DP 数组初始化
dp[0] = 1; // 凑成金额 0 的组合数为 1
// 4. 动态规划填充 DP 数组
for (int i = 0; i < coinsSize; i++) {
for (int j = coins[i]; j <= amount; j++) {
dp[j] += dp[j - coins[i]]; // 状态转移方程
}
}
// 5. 返回值
return dp[amount];
}
2.用unsigned long long。。。
int change(int amount, int* coins, int coinsSize) {
// 1. DP 数组定义
// dp[j] 表示凑成金额 j 的组合数
unsigned long long dp[amount + 1];
memset(dp, 0, sizeof(dp)); // 初始化为 0
// 2. DP 数组初始化
dp[0] = 1; // 凑成金额 0 的组合数为 1
// 3. 动态规划填充 DP 数组
for (int i = 0; i < coinsSize; i++) {
for (int j = coins[i]; j <= amount; j++) {
dp[j] += dp[j - coins[i]]; // 状态转移方程
}
}
// 4. 返回值
return dp[amount];
}
377.组合总和Ⅳ
思路:完全背包
和上一题(518. 零钱兑换 II)思路类似,不同点在于遍历顺序不同,具体如下:
1. DP 数组定义
dp[i]
表示凑成目标值i
的组合数。
2. DP 数组初始化
dp[0] = 1
:表示凑成目标值0
的组合数为 1(不选任何数字)。
3. 动态规划填充 DP 数组
- 外层循环遍历目标值
i
(从1
到target
)。 - 内层循环遍历数组
nums
:- 如果
nums[j] <= i
,则dp[i] += dp[i - nums[j]]
。
- 如果
4. 返回值
- 返回
dp[target]
,表示凑成目标值target
的组合数。
代码
int combinationSum4(int* nums, int numsSize, int target) {
// 1. DP 数组定义
// dp[i] 表示凑成目标值 i 的组合数
unsigned long long dp[target + 1];
memset(dp, 0, sizeof(dp)); // 初始化为 0
// 2. DP 数组初始化
dp[0] = 1; // 凑成目标值 0 的组合数为 1
// 3. 动态规划填充 DP 数组
for (int i = 1; i <= target; i++) {
for (int j = 0; j < numsSize; j++) {
if (nums[j] <= i) {
dp[i] += dp[i - nums[j]]; // 状态转移方程
}
}
}
// 4. 返回值
return dp[target];
}
零钱兑换
问题描述
给定一个硬币数组 coins
和一个目标金额 amount
,计算凑成目标金额所需的最少硬币数。如果无法凑成目标金额,返回 -1
。
思路:完全背包
1.问题转化
- 将硬币视为物品,每种硬币可以无限次使用(完全背包问题)。
- 我们需要找到最少的硬币数,使得它们的总和等于
amount
。
2. DP 数组定义
dp[j]
表示凑成金额j
所需的最少硬币数。valid[j]
表示是否可以凑成金额j
。
3. 检查是否可以凑成目标金额
- 使用
valid
数组记录是否可以凑成金额j
。 - 对于每个硬币
coins[i]
,更新valid[j]
:valid[j] = valid[j] | valid[j - coins[i]]
。
4. DP 数组初始化
dp[0] = 0
:表示凑成金额0
所需的硬币数为 0。- 其他
dp[j]
初始化为amount + 1
,表示初始状态下无法凑成金额j
。
5. 动态规划填充 DP 数组
- 外层循环遍历硬币(物品)。
- 内层循环正序遍历金额(从
coins[i]
到amount
)。 - 对于每个硬币
coins[i]
,更新dp[j]
:dp[j] = min(dp[j], dp[j - coins[i]] + 1)
。
6. 返回值
- 返回
dp[amount]
,表示凑成金额amount
所需的最少硬币数。
代码
int coinChange(int* coins, int coinsSize, int amount) {
// 1. DP 数组定义
// dp[j] 表示凑成金额 j 所需的最少硬币数
int dp[amount + 1];
for (int i = 0; i <= amount; i++) {
dp[i] = amount + 1; // 初始化为一个较大的值
}
// valid[j] 表示是否可以凑成金额 j
bool valid[amount + 1];
memset(valid, false, sizeof(valid)); // 初始化为 false
valid[0] = true; // 金额 0 可以被凑成
// 2. 检查是否可以凑成目标金额
for (int i = 0; i < coinsSize; i++) {
for (int j = coins[i]; j <= amount; j++) {
valid[j] = valid[j] | valid[j - coins[i]]; // 更新 valid 数组
}
}
// 如果无法凑成目标金额,直接返回 -1
if (!valid[amount]) return -1;
// 3. DP 数组初始化
dp[0] = 0; // 凑成金额 0 所需的硬币数为 0
// 4. 动态规划填充 DP 数组
for (int i = 0; i < coinsSize; i++) {
for (int j = coins[i]; j <= amount; j++) {
dp[j] = fmin(dp[j], dp[j - coins[i]] + 1); // 状态转移方程
}
}
// 5. 返回值
return dp[amount];
}
279.完全平方数
思路:完全背包、动态规划
这是一个典型的 完全背包问题,因为每个完全平方数可以无限次使用。我们需要找到最少数量的完全平方数,使得它们的和等于 nn。
1.动态规划定义
dp[j]
:表示组成数字 j所需的最少完全平方数的数量。- 初始化:
dp[j]
初始化为j
,表示最坏情况下,数字 j 可以由 j 个 1 组成(因为 1 是完全平方数)。
2.状态转移方程
对于每个完全平方数
i
2
i^2
i2,更新 dp[j]
:
- 如果
j
>
=
i
2
j>=i^2
j>=i2,则
dp[j] = min(dp[j], dp[j - i*i] + 1)
。 - 解释:如果选择当前完全平方数
i
2
i^2
i2,则组成数字 j 所需的最少完全平方数为
dp[j - i*i] + 1
。
3.遍历顺序
- 外层循环:遍历所有可能的完全平方数 i 2 i^2 i2,其中 i 从 1 到 n \sqrt{n} n。
- 内层循环:正序遍历数字 j(从
i
2
i^2
i2 到 n)。
- 正序遍历是因为每个完全平方数可以无限次使用(完全背包问题)。
4.返回值
- 返回
dp[n]
,表示组成数字 n 所需的最少完全平方数的数量。
代码
int numSquares(int n) {
// 1. 初始化 DP 数组
int dp[n + 1];
for (int i = 0; i <= n; i++) {
dp[i] = i; // 初始化为最坏情况,即全部由 1 组成
}
// 2. 动态规划填充 DP 数组
for (int i = 1; i * i <= n; i++) {
for (int j = i * i; j <= n; j++) {
dp[j] = fmin(dp[j], dp[j - i * i] + 1); // 状态转移方程
}
}
// 3. 返回结果
return dp[n];
}
139.单词拆分
题目描述
给定一个字符串 s
和一个单词字典 wordDict
,判断 s
是否可以由 wordDict
中的单词拼接而成。字典中的单词可以重复使用。
示例:
- 输入:
s = "leetcode"
,wordDict = ["leet", "code"]
- 输出:
true
- 解释:
"leetcode"
可以由"leet"
和"code"
拼接而成。
思路:动态规划,完全背包问题
字典中的单词可以重复使用,所以这是个完全背包问题
1. 动态规划定义
dp[j]
:表示字符串s
的前j
个字符是否可以由wordDict
中的单词拼接而成。- 初始化:
dp[0] = true
:表示空字符串可以被拼接。- 其他
dp[j]
初始化为false
。
2. 状态转移方程
对于每个位置 j
,遍历字典中的每个单词 wordDict[i]
:
- 如果
wordDict[i]
是s[j-wordLen:j]
的子串,则更新dp[j]
:dp[j] = dp[j] || dp[j - wordLen]
。
3. 遍历顺序
- 外层循环:遍历字符串
s
的每个位置j
(从1
到n
)。 - 内层循环:遍历字典中的每个单词
wordDict[i]
。
4. 返回值
- 返回
dp[n]
,表示整个字符串s
是否可以被拼接。
代码
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
// 判断字符串 s 是否可以由 wordDict 中的单词拼接而成
bool wordBreak(char* s, char** wordDict, int wordDictSize) {
int n = strlen(s);
bool dp[n + 1]; // dp 数组大小为 n + 1
memset(dp, false, sizeof(dp));
dp[0] = true; // 空字符串可以被拼接
for (int j = 1; j <= n; j++) { // 遍历字符串 s 的每个位置
for (int i = 0; i < wordDictSize; i++) { // 遍历 wordDict 中的每个单词
int wordLen = strlen(wordDict[i]);
if (j < wordLen) continue; // 如果单词长度大于当前子串长度,跳过
// 检查 wordDict[i] 是否是 s[j-wordLen:j] 的子串
bool flag = true;
for (int k = 0; k < wordLen; k++) {
if (s[j - wordLen + k] != wordDict[i][k]) {
flag = false;
break;
}
}
// 如果 wordDict[i] 是 s[j-wordLen:j] 的子串,更新 dp[j]
if (flag) {
dp[j] = dp[j] || dp[j - wordLen];
}
}
}
return dp[n]; // 返回整个字符串 s 是否可以被拼接
}
回溯代码(若没想到dp,可以用回溯,拿到一部分分数也不错)
bool wordBreak(char* s, char** wordDict, int wordDictSize) {
// 如果 s 为空字符串,返回 true
if (s[0] == '\0') {
return true;
}
bool res = false;
for (int i = 0; i < wordDictSize; i++) {
// 如果 wordDict[i] 比 s 长,跳过
if (strlen(wordDict[i]) > strlen(s)) {
continue;
}
// 检查 wordDict[i] 是否是 s 的前缀
bool flag = true;
for (int j = 0; wordDict[i][j] != '\0'; j++) {
if (s[j] != wordDict[i][j]) {
flag = false;
break;
}
}
// 如果是前缀,递归检查剩余部分
if (flag) {
res = wordBreak(s + strlen(wordDict[i]), wordDict, wordDictSize);
if (res) {
return true; // 如果找到匹配,直接返回 true
}
}
}
return res; // 如果没有找到匹配,返回 false
}
背包问题详细总结
1. 0/1 背包
问题描述
- 特点:每个物品最多只能选一次。
- 目标:在不超过背包容量 V V V 的条件下,使得背包内物品的总价值最大。
状态定义
- 二维状态:
- d p [ i ] [ j ] dp[i][j] dp[i][j] 表示前 i i i 个物品,在总容量为 j j j 时的最大价值。
- 一维状态(空间优化):
- d p [ j ] dp[j] dp[j] 表示容量为 j j j 时的最大价值。
状态转移方程
-
二维版:
d p [ i ] [ j ] = { d p [ i − 1 ] [ j ] (不选第 i 个物品) d p [ i − 1 ] [ j − w i ] + v i (选第 i 个物品, 需满足 j ≥ w i ) dp[i][j] = \begin{cases} dp[i-1][j] & \text{(不选第 $i$ 个物品)} \\ dp[i-1][j-w_i] + v_i & \text{(选第 $i$ 个物品, 需满足 $j \ge w_i$)} \end{cases} dp[i][j]={dp[i−1][j]dp[i−1][j−wi]+vi(不选第 i 个物品)(选第 i 个物品, 需满足 j≥wi)
归纳为:
d p [ i ] [ j ] = max ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w i ] + v i ) ( j ≥ w i ) dp[i][j] = \max\Big(dp[i-1][j],\ dp[i-1][j-w_i] + v_i\Big) \quad (j \ge w_i) dp[i][j]=max(dp[i−1][j], dp[i−1][j−wi]+vi)(j≥wi) -
一维版(倒序更新):
for i = 1 to n : for j = V downto w i : d p [ j ] = max ( d p [ j ] , d p [ j − w i ] + v i ) \text{for } i = 1 \text{ to } n:\\ \quad \text{for } j = V \text{ downto } w_i:\\ \quad\quad dp[j] = \max\big(dp[j],\ dp[j-w_i] + v_i\big) for i=1 to n:for j=V downto wi:dp[j]=max(dp[j], dp[j−wi]+vi)
迭代顺序
- 倒序遍历容量 j j j,以确保每个物品只被使用一次,避免重复计入当前物品。
注意事项
- 更新顺序:使用一维数组时,必须从大到小遍历容量 j j j,否则会重复使用同一物品。
- 初始状态:通常 d p [ 0 ] = 0 dp[0] = 0 dp[0]=0,其他状态应根据问题情况初始化(如不可达状态可以初始化为负无穷)。
- 边界检查:确保在访问 d p [ j − w i ] dp[j-w_i] dp[j−wi] 前,容量 j ≥ w i j \ge w_i j≥wi。
2. 完全背包
问题描述
- 特点:每个物品可以无限次选取。
- 目标:在背包容量 V V V 内,选取物品使得总价值最大,并允许每个物品多次选用。
状态定义
- 与 0/1 背包类似,使用 d p [ i ] [ j ] dp[i][j] dp[i][j] 或优化为 d p [ j ] dp[j] dp[j]。
状态转移方程
-
二维版:
d p [ i ] [ j ] = max ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − w i ] + v i ) ( j ≥ w i ) dp[i][j] = \max\Big(dp[i-1][j],\ dp[i][j-w_i] + v_i\Big) \quad (j \ge w_i) dp[i][j]=max(dp[i−1][j], dp[i][j−wi]+vi)(j≥wi)注意:这里使用了 d p [ i ] [ j − w i ] dp[i][j-w_i] dp[i][j−wi] 表明同一物品可以被重复使用。
-
一维版(正序更新):
for i = 1 to n : for j = w i to V : d p [ j ] = max ( d p [ j ] , d p [ j − w i ] + v i ) \text{for } i = 1 \text{ to } n:\\ \quad \text{for } j = w_i \text{ to } V:\\ \quad\quad dp[j] = \max\big(dp[j],\ dp[j-w_i] + v_i\big) for i=1 to n:for j=wi to V:dp[j]=max(dp[j], dp[j−wi]+vi)
迭代顺序
- 正序遍历容量 j j j,确保在更新状态时可以多次利用当前物品的状态。
注意事项
- 遍历顺序:必须正序遍历容量 j j j,否则无法保证同一物品能够被重复选用。
- 初始化:同样需要正确初始化 d p [ 0 ] = 0 dp[0]=0 dp[0]=0,其他状态根据问题定义设置。
- 状态依赖:在更新时注意依赖的是当前物品的状态(即 d p [ j − w i ] dp[j-w_i] dp[j−wi] 在同一轮中可能已经更新)。
3. 多重背包(有限个数背包)
问题描述
- 特点:每个物品有一个有限的选取次数 c n t i cnt_i cnti(既不无限也不只有一次)。
- 目标:在背包容量 V V V 内,选取物品使得总价值最大,同时满足每个物品最多选取 c n t i cnt_i cnti 次。
状态定义
- 同样可以用 d p [ i ] [ j ] dp[i][j] dp[i][j] 或优化为一维数组 d p [ j ] dp[j] dp[j] 表示最大价值。
状态转移方程
-
直接枚举法:
d p [ i ] [ j ] = max 0 ≤ k ≤ c n t i , j ≥ k × w i { d p [ i − 1 ] [ j − k × w i ] + k × v i } dp[i][j] = \max_{0 \le k \le cnt_i,\; j \ge k \times w_i} \Big\{ dp[i-1][j-k \times w_i] + k \times v_i \Big\} dp[i][j]=0≤k≤cnti,j≥k×wimax{dp[i−1][j−k×wi]+k×vi}
或在一维数组中进行类似枚举(需倒序遍历保证不重复使用)。 -
二进制拆分法:
- 将每个物品的数量 c n t i cnt_i cnti 拆分为若干个数量为 1 , 2 , 4 , … , r 1, 2, 4, \ldots, r 1,2,4,…,r 的物品,使得这些数量之和等于 c n t i cnt_i cnti。
- 拆分后每个“子物品”视为 0/1 背包问题处理(使用倒序遍历)。
-
单调队列优化(高级技巧):
- 在某些特定场景下,可以利用单调队列优化内层循环,减少枚举次数。
迭代顺序
- 直接枚举法:外层枚举物品,内层枚举每个物品可选的数量 k k k,同时对容量进行倒序遍历。
- 二进制拆分后:将问题转化为多个 0/1 背包问题,使用倒序遍历更新。
实现代码
#include <stdio.h>
#include <stdlib.h>
// 取两个数的最小值
int min_int(int a, int b) {
return a < b ? a : b;
}
/*
* 函数名: multiKnapsackDirect
* 功能 : 多重背包问题的直接枚举实现(朴素方法)。
* 参数 :
* - n : 物品总数
* - V : 背包总容量
* - weight : 物品重量数组,下标 0 ~ n-1
* - value : 物品价值数组,下标 0 ~ n-1
* - cnt : 每个物品可选次数数组,下标 0 ~ n-1
* - dp : dp 数组,大小为 V+1,存储每个容量下的最优解(需预先初始化)
*
* 说明:
* 对于每个物品 i,遍历背包容量 j(从大到小),并枚举选取该物品的数量 k(1~cnt[i])。
* 其中倒序遍历确保同一物品不会被重复选用。
*/
void multiKnapsackDirect(int n, int V, int weight[], int value[], int cnt[], int dp[]) {
// 遍历每个物品
for (int i = 0; i < n; i++) {
// 倒序遍历背包容量,防止重复使用同一物品
for (int j = V; j >= 0; j--) {
// 枚举选取该物品的数量 k(从1到cnt[i])
for (int k = 1; k <= cnt[i]; k++) {
int totalWeight = k * weight[i]; // 当前选取 k 个物品的总重量
// 如果当前容量 j 足够容纳 k 个该物品,则尝试更新 dp[j]
if (j >= totalWeight) {
int candidate = dp[j - totalWeight] + k * value[i];
if (candidate > dp[j]) {
dp[j] = candidate;
}
} else {
// 如果当前重量超过 j,后续更大 k 的值也一定不满足
break;
}
}
}
}
}
/*
* 函数名: multiKnapsackBinarySplitting
* 功能 : 多重背包问题的二进制拆分优化实现。
* 参数 :
* - n : 物品总数
* - V : 背包总容量
* - weight : 物品重量数组,下标 0 ~ n-1
* - value : 物品价值数组,下标 0 ~ n-1
* - cnt : 每个物品可选次数数组,下标 0 ~ n-1
* - dp : dp 数组,大小为 V+1,存储每个容量下的最优解(需预先初始化)
*
* 说明:
* 将每个物品的数量 cnt[i] 拆分成若干个数量为 1, 2, 4, ... 的组,
* 转化为若干个 0/1 背包问题进行处理,从而降低内层循环的次数。
*/
void multiKnapsackBinarySplitting(int n, int V, int weight[], int value[], int cnt[], int dp[]) {
// 遍历每个物品
for (int i = 0; i < n; i++) {
int quantity = cnt[i]; // 当前物品剩余可选数量
int k = 1; // 拆分因子,依次为 1, 2, 4, ...
// 对当前物品进行二进制拆分
while (quantity > 0) {
// 取当前组的数量,不超过剩余数量
int num = min_int(k, quantity);
quantity -= num;
int totalWeight = num * weight[i]; // 当前组总重量
int totalValue = num * value[i]; // 当前组总价值
// 0/1 背包更新:倒序遍历容量,保证当前拆分组只用一次
for (int j = V; j >= totalWeight; j--) {
int candidate = dp[j - totalWeight] + totalValue;
if (candidate > dp[j]) {
dp[j] = candidate;
}
}
k *= 2; // 翻倍
}
}
}
/*
* 函数名: multiKnapsackMonotonicQueue
* 功能 : 多重背包问题的单调队列优化实现(适用于大容量、多重物品)。
* 参数 :
* - n : 物品总数
* - V : 背包总容量
* - weight : 物品重量数组,下标 0 ~ n-1
* - value : 物品价值数组,下标 0 ~ n-1
* - cnt : 每个物品可选次数数组,下标 0 ~ n-1
* - dp : dp 数组,大小为 V+1,存储每个容量下的最优解(需预先初始化)
*
* 说明:
* 对于当前物品 i,其重量为 w,价值为 v,数量为 cnt,
* 对所有满足 j ≡ r (mod w) 的背包容量 j(记 j = r + t*w,t 为非负整数)进行如下转移:
*
* dp[r + t*w] = max_{s in [max(0, t-cnt), t]} { dp[r + s*w] - s*v } + t*v
*
* 为高效求解右侧窗口内的最大值,采用单调队列优化。
*/
void multiKnapsackMonotonicQueue(int n, int V, int weight[], int value[], int cnt[], int dp[]) {
// 遍历每个物品
for (int i = 0; i < n; i++) {
int w = weight[i]; // 当前物品重量
int v = value[i]; // 当前物品价值
int num = cnt[i]; // 当前物品数量限制
// 对于每个余数 r (0 <= r < w),处理 j = r, r+w, r+2w, ... 不超过 V 的所有容量
for (int r = 0; r < w; r++) {
// 定义双端队列,用于维护区间最大值(队列中存储 t 的值,t 表示背包中“层数”,即 j = r + t*w)
// 为简单起见,这里分配的数组大小取 (V/w + 10),确保足够存储
int *deque = (int *)malloc(sizeof(int) * (V / w + 10));
int head = 0, tail = -1; // 初始化队列为空
// 计算 t 的最大值:满足 r + t*w <= V
int maxT = (V - r) / w;
// 遍历每个 t,对应背包容量 j = r + t*w
for (int t = 0; t <= maxT; t++) {
int j = r + t * w; // 当前容量位置
// 计算函数 f(t) = dp[j] - t*v
int cur_f = dp[j] - t * v;
// 移除队列头部的元素,如果其对应的 t 小于当前 t - num,
// 意味着该元素不在滑动窗口 [t - num, t] 内(窗口大小为 num+1)
while (head <= tail && deque[head] < t - num) {
head++;
}
// 维护队列单调性:
// 队列中存储的 t 值对应的 f(t) 值应按递减顺序排列
// 移除队列尾部所有 f 值不大于当前 cur_f 的元素
while (head <= tail) {
int last_t = deque[tail];
int last_f = dp[r + last_t * w] - last_t * v;
if (last_f <= cur_f) {
tail--;
} else {
break;
}
}
// 将当前 t 加入队列尾部
deque[++tail] = t;
// 队列头部存储的 t 值即为窗口内 f(t) 的最大值对应的下标
int best_t = deque[head];
// 根据公式更新 dp[j]:dp[j] = f(best_t) + t*v
dp[j] = dp[r + best_t * w] - best_t * v + t * v;
}
free(deque);
}
}
}
注意事项
- 效率问题:直接枚举 k k k 可能会使时间复杂度过高,建议使用二进制拆分来优化。
- 拆分细节:二进制拆分时要注意剩余部分的处理(当 c n t i cnt_i cnti 不是 2 的幂次时)。
- 边界检查:同样确保在更新状态前满足 j ≥ k × w i j \ge k \times w_i j≥k×wi 的条件。
三者比较
特性 | 0/1 背包 | 完全背包 | 多重背包 |
---|---|---|---|
选取限制 | 每个物品最多选一次 | 每个物品可无限次选取 | 每个物品最多选取 c n t i cnt_i cnti 次(有限,但可能大于1) |
状态转移(二维) | d p [ i ] [ j ] = max ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w i ] + v i ) dp[i][j]=\max\Big(dp[i-1][j],\; dp[i-1][j-w_i]+v_i\Big) dp[i][j]=max(dp[i−1][j],dp[i−1][j−wi]+vi) | d p [ i ] [ j ] = max ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − w i ] + v i ) dp[i][j]=\max\Big(dp[i-1][j],\; dp[i][j-w_i]+v_i\Big) dp[i][j]=max(dp[i−1][j],dp[i][j−wi]+vi) | d p [ i ] [ j ] = max 0 ≤ k ≤ c n t i , j ≥ k w i { d p [ i − 1 ] [ j − k w i ] + k v i } dp[i][j]=\max_{0\le k\le cnt_i,\; j\ge k w_i}\Big\{dp[i-1][j-k w_i]+k v_i\Big\} dp[i][j]=0≤k≤cnti,j≥kwimax{dp[i−1][j−kwi]+kvi} |
状态转移(一维) | 倒序更新: for j = V downto w i : d p [ j ] = max ( d p [ j ] , d p [ j − w i ] + v i ) \text{for } j=V \text{ downto } w_i:\; dp[j]=\max\big(dp[j],\; dp[j-w_i]+v_i\big) for j=V downto wi:dp[j]=max(dp[j],dp[j−wi]+vi) | 正序更新: for j = w i to V : d p [ j ] = max ( d p [ j ] , d p [ j − w i ] + v i ) \text{for } j=w_i \text{ to } V:\; dp[j]=\max\big(dp[j],\; dp[j-w_i]+v_i\big) for j=wi to V:dp[j]=max(dp[j],dp[j−wi]+vi) | 直接枚举: for j = V downto 0 : for k = 0 to c n t i (if j ≥ k w i ) : d p [ j ] = max ( d p [ j ] , d p [ j − k w i ] + k v i ) \text{for } j=V \text{ downto } 0:\\ \quad \text{for } k=0 \text{ to } cnt_i \text{ (if } j\ge k w_i):\; dp[j]=\max\big(dp[j],\; dp[j-k w_i]+k v_i\big) for j=V downto 0:for k=0 to cnti (if j≥kwi):dp[j]=max(dp[j],dp[j−kwi]+kvi) 或使用二进制拆分后转为 0/1 背包 |
迭代顺序 | 容量 j j j 倒序遍历 | 容量 j j j 正序遍历 | 视实现方式:直接枚举时容量倒序;二进制拆分后为倒序(0/1 背包方式) |
时间复杂度 | O ( n V ) O(nV) O(nV) | O ( n V ) O(nV) O(nV) | 直接枚举:
O
(
n
⋅
c
n
t
i
⋅
V
)
O(n \cdot cnt_i \cdot V)
O(n⋅cnti⋅V) 二进制拆分: O ( n log ( c n t i ) ⋅ V ) O(n\log(cnt_i)\cdot V) O(nlog(cnti)⋅V) |
常用技巧 | 一维数组优化、边界条件处理 | 一维数组优化、正序更新确保无限选 | 二进制拆分、单调队列优化(高级) |
总结与注意事项
-
更新顺序至关重要
- 0/1 背包:必须倒序遍历容量,防止在同一轮中重复选取物品。
- 完全背包:需要正序遍历容量,保证同一物品可以多次选取。
- 多重背包:直接枚举时同样需要倒序遍历;若使用二进制拆分,则问题转化为多个 0/1 背包问题,同样采用倒序遍历。
-
状态初始化
- 通常将 d p [ 0 ] dp[0] dp[0] 初始化为 0,其它状态根据具体问题设定(有时不可达的状态需设为负无穷)。
-
边界条件处理
- 在更新 d p [ j ] dp[j] dp[j] 前,应检查 j ≥ w i j \ge w_i j≥wi 或 j ≥ k × w i j \ge k \times w_i j≥k×wi,以防数组越界或错误更新。