13 0-1背包&完全背包
0-1背包问题
有 \(n\) 个物品,第 \(i\) 个物品的体积为 \(w[i]\),价值为 \(v[i]\)。每个物品至多选一个,求体积和不超过 \(capacity\) 的最大价值和。
回溯三问
- 当前操作? 枚举第 \(i\) 个物品选或不选。不选,剩余容量不变;选,剩余容量减少 \(w[i]\)。
- 子问题? 在剩余容量为 \(c\) 时,从前 \(i\) 个物品中得到的最大价值和。
- 下一个子问题? 分类讨论:
- 不选:在剩余容量为 \(c\) 时,从前 \(i-1\) 个物品中得到的最大价值和;
- 选:在剩余容量为 \(c-w[i]\) 时,从前 \(i-1\) 个物品中得到的最大价值和。
综上可得:\(dfs(i, c) = max(dfs(i - 1, c), dfs(i - 1, c - w[i]) + v[i])\)
常见变形:
- 至多装 \(capacity\),求最大价值和;
- 恰好装 \(capacity\),求最大/最小价值和;
- 至少装 \(capacity\),求最小价值和
代码实现
点击查看代码
int zero_one_knapsack(int capacity, vector<int>& w, vector<int>& v) {
int n = w.size();
auto dfs = [&](int i, int c) -> int {
if (i < 0) {
return 0;
}
if (c < w[i]) {
return dfs(i - 1, c);
}
return max(dfs(i - 1, c), dfs(i - 1, c - w[i]) + v[i]);
};
return dfs(n - 1, capacity);
}
- 时间复杂度:\(O(2^n)\)
- 空间复杂度:\(O(n)\)
1 目标和

1.1 解题思路
假设添加正号的和为 \(p\),那么添加负数的和为 \(s-p\)(\(s\) 为数组的和,所以 \(s-p\) 相当于负数和的绝对值),于是有 \(p-(s-p) = target\),进而可以得到: $$p = \frac{target + s}{2}$$
这样的话问题就变成从 \(nums\) 中选择一些数字,使其和 \(p\) 恰好等于 \((target + s)/2\)的方案数。
1.2 代码实现
点击查看代码
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int s = reduce(nums.begin(), nums.end(), 0);
if (s + target < 0 || (s + target) % 2 != 0) {
return 0;
}
int n = nums.size();
auto dfs = [&](this auto&& dfs, int i, int sum) -> int {
if (i < 0) {
if (sum == 0) {
return 1;
}
return 0;
}
if (sum < nums[i]) {
return dfs(i - 1, sum);
}
return dfs(i - 1, sum) + dfs(i - 1, sum - nums[i]);
};
return dfs(n - 1, (target + s) / 2);
}
};
- 时间复杂度:\(O(2^n)\)
- 空间复杂度:\(O(n)\)
记忆化搜索可以优化时间复杂度到 \(O(n)\),那么能不能优化空间复杂度到 \(O(1)\) 呢?
改成递推。
已知 $$dfs(i, sum) = dfs(i - 1, sum) + dfs(i - 1, sum - nums[i])$$,进一步可以得到 $$ f[i][sum] = f[i - 1][sum] + f[i - 1][sum - nums[i]]$$,
所以有:
以下是递推的做法。
点击查看代码
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int s = reduce(nums.begin(), nums.end(), 0);
int t = s + target;
if (t < 0 || t % 2 != 0) {
return 0;
}
t = t / 2;
int n = nums.size();
// 初始化 $f$ 数组
vector<vector<int>> f(n + 1, vector<int>(t + 1, 0));
f[0][0] = 1;
for (int i = 0; i < n; ++i) {
for (int j = 0; j <= t; ++j) {
if (j < nums[i]) {
f[i + 1][j] = f[i][j];
} else {
f[i + 1][j] = f[i][j] + f[i][j - nums[i]];
}
}
}
return f[n][t];
}
};
- 时间复杂度:\(O(n*t)\)
- 空间复杂度:\(O(n*t)\)
继续优化空间复杂度的思路:
- 滚动数组(两个数组)
每次计算出 \(f[i-1]\) 后,后面就不会用到 \(f[i]\) 了。换句话说,每时每刻,只有两行数组在参与运算。比如把 \(f[1]\) 算完了,那么 \(f[0]\) 就用不到了,用 \(f[1]\) 推导 \(f[2]\),再将 \(f[2]\) 的结果覆盖 \(f[0]\)。
点击查看代码
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int s = reduce(nums.begin(), nums.end(), 0);
int t = s + target;
if (t < 0 || t % 2 != 0) {
return 0;
}
t = t / 2;
int n = nums.size();
// 滚动数组
// 初始化 $f$ 数组
vector<vector<int>> f(2, vector<int>(t + 1, 0));
f[0][0] = 1;
for (int i = 0; i < n; ++i) {
for (int j = 0; j <= t; ++j) {
if (j < nums[i]) {
f[(i + 1)%2][j] = f[i%2][j];
} else {
f[(i + 1)%2][j] = f[i%2][j] + f[i%2][j - nums[i]];
}
}
}
return f[n%2][t];
}
};
- 时间复杂度:\(O(n*t)\)
- 空间复杂度:\(O(t)\)
- 一个数组
进一步可以发现,\(f[i + 1][j]\) 主要依赖于 \(f[i][j]\) 和 \(f[i][j - nums[i]]\),所以可以考虑从后向前更新。
点击查看代码
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int s = reduce(nums.begin(), nums.end(), 0);
int t = s + target;
if (t < 0 || t % 2 != 0) {
return 0;
}
t = t / 2;
int n = nums.size();
// 滚动数组
// 初始化 $f$ 数组
vector<int> f(t + 1, 0);
f[0] = 1;
for (int i = 0; i < n; ++i) {
for (int j = t; j >= nums[i]; --j) {
f[j] = f[j] + f[j - nums[i]];
}
}
return f[t];
}
};
完全背包
有 \(n\) 种物品, 第 \(i\) 种物品的体积为 \(w[i]\),价值为 \(v[i]\),每种物品无限次重复选,求体积和不超过 \(capacity\) 的最大价值和。
回溯三问:
- 当前操作? 枚举第 \(i\) 种物品选一个或不选;不选,剩余容量不变;选一个,剩余容量减少 \(w[i]\)
- 子问题? 在剩余容量为 \(c\) 时,从前 \(i\) 个物品中得到的最大价值和
- 下一个子问题? 分类讨论:
- 不选:在剩余容量为 \(c\) 时,从前 \(i - 1\) 种物品中得到的最大价值和;
- 选:在剩余容量为 \(c-w[i]\) 时,从前 \(i\) 🀄物品中得到的最大价值和。
综上,可以得到:$$dfs(i,c)=max(dfs(i-1,c), dfs(i, c-w[i])+v[i])$$
2 零钱兑换

2.1 代码实现
回溯:
点击查看代码
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n = coins.size();
auto dfs = [&](this auto&&dfs, int i, int c) -> int {
if (i < 0) {
return c == 0 ? 0: INT_MAX / 2;
} // 0表示合法,INT_MAX / 2 表示不合法
if (coins[i] > c) {
return dfs(i - 1, c);
}
return min(dfs(i - 1, c), dfs(i, c - coins[i]) + 1);
};
int res = dfs(n - 1, amount);
return res < INT_MAX / 2 ? res: -1;
}
};
记忆化搜索:
点击查看代码
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n = coins.size();
vector<vector<int>> dp(n, vector<int>(amount + 1, -1));
auto dfs = [&](this auto&&dfs, int i, int c) -> int {
if (i < 0) {
return c == 0 ? 0: INT_MAX / 2;
} // 0表示合法,INT_MAX / 2 表示不合法
auto& res = dp[i][c];
if (res != -1) {
return res;
}
if (coins[i] > c) {
res = dfs(i - 1, c);
} else {
res = min(dfs(i - 1, c), dfs(i, c - coins[i]) + 1);
}
return res;
};
int res = dfs(n - 1, amount);
return res < INT_MAX / 2 ? res: -1;
}
};
- 时间复杂度:\(O(n * amount)\)
- 空间复杂度:\(O(n * amount)\)
改成递推
点击查看代码
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n = coins.size();
// 初始化
vector<vector<int>> f(n + 1, vector<int>(amount + 1, INT_MAX / 2));
f[0][0] = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < amount + 1; ++j) {
if (coins[i] > j) {
f[i+1][j] = f[i][j];
} else {
f[i+1][j] = min(f[i][j], f[i+1][j-coins[i]] + 1);
}
}
}
int res = f[n][amount];
return res < INT_MAX / 2 ? res: -1;
}
};
- 时间复杂度:\(O(n*amount)\)
- 空间复杂度:\(O(n*amount)\)
点击查看代码
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n = coins.size();
// 初始化
vector<int> f(amount + 1, INT_MAX / 2);
f[0] = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < amount + 1; ++j) {
if (coins[i] > j) {
f[j] = f[j];
} else {
f[j] = min(f[j], f[j-coins[i]] + 1);
}
}
}
int res = f[amount];
return res < INT_MAX / 2 ? res: -1;
}
};
- 时间复杂度:\(O(n*amount)\)
- 空间复杂度:\(O(amount)\)
0-1背包是倒序,完全背包是正序,那么怎么思考循环的顺序呢?

浙公网安备 33010602011771号