我在刷题的时候学习到了“完全背包”问题。但是当遇到“完全背包 + 排列数” 或者 “完全背包 + 组合数” 就不太确定到底是:先物品,再背包(就是横着);还是 先背包,再物品,从小到大遍历(就是竖着) 进行遍历。
然后我搜索到这样的话:
结论一:
- 一般而言,先行或者先列都可以。但是有的不行。
- 组合问题的遍历顺序是:先物品,再背包,从小到大遍历(就是横着)。
先放物品的题目:零钱兑换 II
- 排列问题的遍历顺序时:先背包,再物品,从小到大遍历(就是竖着)。
先循环背包的题目:组合总和 Ⅳ
- 如果排解和组合都可以,那就随便。如:零钱兑换
但经过我的实验发现:对于完全背包问题,二维 DP 可以无视顺序。上面这个顺序是对于代码简化后的一维 DP 。
组合示例
这里以 零钱兑换 II 题目为例子,按照一般的理论,需要 “先物品,再背包” 。但经过验证发现,无论是哪种顺序的遍历都可以。下面是代码,大家可以实测。
// 代码一:先 i 后 j 遍历
class Solution {
public:
int change(int amount, vector<int>& coins) {
int dp[3001][5001] = {0};
int n = coins.size();
for(int i = 0; i <= n; i++) dp[i][0] = 1;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= amount; j++ ) {
if(coins[i - 1] > j) dp[i][j] = dp[i - 1][j];
else dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]];
}
}
return dp[n][amount];
}
};
// 代码二:先 j 后 i 遍历
class Solution {
public:
int change(int amount, vector<int>& coins) {
int dp[3001][5001] = {0};
int n = coins.size();
for(int i = 0; i <= n; i++) dp[i][0] = 1;
for(int j = 1; j <= amount; j++) { // int i = 1; i <= n; i++
for(int i = 1; i <= n; i++) {
if(coins[i - 1] > j) dp[i][j] = dp[i - 1][j];
else dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]];
}
}
return dp[n][amount];
}
};
排列示例
组合总和 Ⅳ是典型的先循环背包的题目。但是经过我的实测发现,在使用二维数组的情况下,遍历顺序也是可以互换的。付代码:
// 代码1;
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
int dp[201][1001] = {0};
int n = nums.size();
for(int i = 0; i <= n; i++) dp[i][0] = 1;
for(int i = 1; i <= n; i++) { // int j = 1; j <= target; j++
for(int j = 1; j <= target; j++) {
for(int k = i; k > 0; k--) {
if(j - nums[k - 1] >= 0 && dp[i][j] < INT_MAX - dp[i][j - nums[k - 1]]) {
dp[i][j] += dp[i][j - nums[k - 1]];
}
}
}
}
return dp[n][target];
}
};
// 代码二:
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
int dp[201][1001] = {0};
int n = nums.size();
for(int i = 0; i <= n; i++) dp[i][0] = 1;
for(int j = 1; j <= target; j++) {
for(int i = 1; i <= n; i++) {
for(int k = i; k > 0; k--) {
if(j - nums[k - 1] >= 0 && dp[i][j] < INT_MAX - dp[i][j - nums[k - 1]]) {
dp[i][j] += dp[i][j - nums[k - 1]];
}
}
}
}
return dp[n][target];
}
};
大致的总结
其实,可以这么理解:“排列的难度” > “组合的难度”。所以在求排列的时候就不能像组合那样无脑查几个值就好,可能得要遍历已有表才行(这也是排列问题多一重循环的原因,排列问题多了第三重循环)。
另外,别人总结的应该也是没有错误的,那他们的结论是如何得到的呢?因为有人想在“排列”中节省一下,不使用第三重循环,就变成了以下的代码3。这个代码是不能够交换 i j 的遍历顺序的。但是代码2 是可以交换的。原因在于:上面的排列代码无论是代码1 还是 代码2,某个值 DP[i][j] 都是由其左上方矩阵中的值完全确定的,但是如果你想省去第三重循环,就得用左下方的值(如代码3),这个时候遍历的顺序肯定是无法交换的。
// 代码3
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<vector<int>> dp(nums.size()+1, vector<int>(target+1,0));
for(int i=0; i<nums.size()+1; i++) dp[i][0] = 1;
for(int j = 1; j<= target; j++){
for(int i = 1; i<=nums.size(); i++){
if(j >= nums[i-1] && dp[nums.size()][j-nums[i-1]] < INT_MAX - dp[i-1][j]) //防止溢出
dp[i][j] = dp[i-1][j] + dp[nums.size()][j-nums[i-1]] ; //更新量 = 不用当前值的方法数 + 包含当前值的方法数(囊括了重复的情况)
else dp[i][j] = dp[i-1][j];
}
}
return dp[nums.size()][target];
}
};
链接:https://leetcode.cn/problems/combination-sum-iv/solutions/1755590/dong-tai-gui-hua-by-strange-6ouldntj-waip/