完全背包问题中二维动态规划的遍历顺序问题

我在刷题的时候学习到了“完全背包”问题。但是当遇到“完全背包 + 排列数” 或者 “完全背包 + 组合数” 就不太确定到底是:先物品,再背包(就是横着);还是 先背包,再物品,从小到大遍历(就是竖着) 进行遍历。
然后我搜索到这样的话:

结论一

  • 一般而言,先行或者先列都可以。但是有的不行。
    - 组合问题的遍历顺序是:先物品,再背包,从小到大遍历(就是横着)。
    先放物品的题目:零钱兑换 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/
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值