动态规划——dp的含义归类(完全背包和01背包区别)
- 用dp表示总价值
- 用dp表示组合数
给出一个总数,一些物品,问能否凑成这个总数。
零钱兑换II https://leetcode.cn/problems/coin-change-ii/
完全背包
二维写法
for (int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
}
}
一维写法
// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j < bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
为什么要先遍历物品,在遍历背包呢? (灵魂拷问)
其实纯完全背包,先遍历物品,再遍历背包 与 先遍历背包,再遍历物品都是可以的。
零钱兑换II 不是纯完全背包(求是否能装满背包),而是求装满背包有几种方法。
这里在遍历顺序上可就有说法了。
- 如果求组合数就是外层for循环遍历物品,内层for遍历背包。
- 如果求排列数就是外层for遍历背包,内层for循环遍历物品。
为什么会有这种区别?
组合数 是不考虑顺序的,即只关心哪些硬币被选中了,不关心它们的顺序。第一个代码块中,每个硬币依次处理,确保了每个硬币只会按照固定顺序被考虑,因此不会重复计算顺序不同但硬币相同的组合。
排列数 是考虑顺序的,即不同顺序的硬币视为不同的方案。第二个代码块中,每次更新金额时,都考虑了所有硬币的排列,因此会计算所有可能的排列。
for (int j = 0; j <= amount; j++) { // 遍历背包容量
for (int i = 0; i < coins.size(); i++) { // 遍历物品
if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
}
}
每次更新容量时,遍历一遍所有物品作为当前可能要选择的物品
一维
遍历的时候和01背包不同,01背包需要内层(容量)倒序遍历。而完全背包不需要。这是因为完全背包的递推公式不一样:
如求组合数的递推公式:
完全背包:
二维递推公式:dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]]
压缩成一维:dp[j] += dp[j - coins[i]]
01背包:
二维递推公式:dp[i][j] = dp[i - 1][j] + dp[i-1][j - coins[i]]
压缩成一维:dp[j] += dp[j - coins[i]]
区别: 01背包需要内层倒序遍历,防止覆盖前一层的值;完全背包不需要倒序遍历。
总结
求组合数:518.零钱兑换II
求排列数:377. 组合总和 Ⅳ 70. 爬楼梯进阶版(完全背包)
求最小数:322. 零钱兑换 、279.完全平方数