力扣 322 零钱兑换
本问题是一个组合问题,目的是获得硬币更少的找零方案。这里初始化需要设置为INT_MAX
,循环中用min
来更新数组。同时开始前需将dp[0]
标记为0
,即最少使用0
个硬币即可得到数额0
。整体代码如下:
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for(int i = 0; i < coins.size(); i++){
for(int j = coins[i]; j <= amount; j++){
if (dp[j - coins[i]] != INT_MAX){
dp[j] = min(dp[j], dp[j - coins[i]] + 1);
}
}
}
return dp[amount] == INT_MAX ? -1 : dp[amount];
}
};
还有一点,INT_MAX + 1
会溢出为负数,导致 min(dp[j], dp[j - coins[i]] + 1)
出错。因此保险起见,循环中需要加上 if
防止溢出。
力扣 279 完全平方数 AC
第一遍ac代码如下:
class Solution {
public:
int numSquares(int n) {
int m = sqrt(n);
vector<int> dp(n + 1);
for(int j = 0; j <= n; j++){
dp[j] = j;
}
for(int i = 2; i <= m; i++){
for(int j = i * i; j <= n; j++){
dp[j] = min(dp[j], dp[j - i * i] + 1);
}
}
return dp[n];
}
};
后发现因为最小值是1
,所以无需额外初始化,可以放到整体的遍历过程里:
class Solution {
public:
int numSquares(int n) {
int m = sqrt(n);
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for(int i = 1; i <= m; i++){
for(int j = i * i; j <= n; j++){
dp[j] = min(dp[j], dp[j - i * i] + 1);
}
}
return dp[n];
}
};
这里在更新第一个物品
1
2
1^2
12 的时候就已经把所有dp[j]
初始化了,所以无需进一步担心循环中可能有INT_MAX + 1
溢出的问题,没加额外判断。
力扣 139 单词拆分
定义dp数组:dp[j]
表示字符串长度为j时,如果可以拆分为字典里的一个或者多个词汇,那么记为true
。本题本质上是排列,因此外层遍历背包,内层遍历物品。但对于本题,内层循环的写法稍有不同,它尝试把
[
0
,
j
−
1
]
[0,j-1]
[0,j−1] 拆成两部分进行判断:
- 前半部分
[
0
,
i
−
1
]
[0,i-1]
[0,i−1] 是否可拆 ,这里使用
dp[i]
判断。 - 后半部分
[
i
,
j
−
1
]
[i,j-1]
[i,j−1] 是否在字典中,即
i
是否为最后一个单词的起点。
相当于dp[i]
是一个已知状态,它表示前i
个字符是否可以拆分,而dp[j]
是一个待判断的新状态,我们正在尝试dp[i]
用来更新它。代码如下:
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
vector<bool> dp(s.size() + 1, false);
dp[0] = true;
for(int j = 1; j <= s.size(); j++){
for(int i = 0; i < j; i++){
string word = s.substr(i, j - i);
if(wordSet.find(word) != wordSet.end() && dp[i]){
dp[j] = true;
}
}
}
return dp[s.size()];
}
};
多重背包问题
给定一个容量为 W 的背包,和 N 种物品。每种物品有:
- 一个重量
weight[i]
- 一个价值
value[i]
- 最多可用的数量
count[i]
要求在不超过背包容量 W
的前提下,选择若干物品放入背包,使得物品的总价值最大。
但事实上这里把count[i]
摊开又是一个0-1背包问题了:
int multipleKnapsack(int bagSize, const vector<int>& weight, const vector<int>& value, const vector<int>& count) {
vector<int> newWeight;
vector<int> newValue;
// 把每个带数量限制的物品拆分成多个 0-1 背包物品
for (int i = 0; i < weight.size(); i++) {
for (int k = 0; k < count[i]; k++) {
newWeight.push_back(weight[i]);
newValue.push_back(value[i]);
}
}
// 经典 0-1 背包(滚动数组)
vector<int> dp(bagSize + 1, 0);
for (int i = 0; i < newWeight.size(); i++) {
for (int j = bagSize; j >= newWeight[i]; j--) {
dp[j] = max(dp[j], dp[j - newWeight[i]] + newValue[i]);
}
}
r
背包问题总结
到这里背包问题基本结束,总结一下遇到题型(cr. 代码随想录):
问题类型 | 状态转移公式 | LeetCode 例题 | 说明 |
---|---|---|---|
✅ 是否能装满背包(或是最多能装多少) | dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]) | 416. 分割等和子集 1049. 最后一块石头的重量 II | 转换成是否存在子集使和为 target |
✅ 装满背包的方案数(组合or排列) | dp[j] += dp[j - nums[i]] | 494. 目标和 518. 零钱兑换 II 377. 组合总和 IV 70. 爬楼梯(完全背包) | 问方案个数,使用 += 叠加方案数,常用于排列/组合问题 |
✅ 装满背包的最大价值(最优解) | dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) | 474. 一和零 | 标准的 0-1 / 完全 / 多重背包结构,求最大价值 |
✅ 装满背包所需的最小物品数(最优解) | dp[j] = min(dp[j], dp[j - coins[i]] + 1) | 322. 零钱兑换 279. 完全平方数 | 用最少物品凑出目标 |