背包问题的遍历顺序

01背包

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
举例:背包最大重量为4。
物品为:

重量价值
物品0115
物品1320
物品2430

问背包能背的物品最大价值是多少?

二维动态规划

dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大为dp[i][j]。

那么可以有两个方向推出来dp[i][j]:

  1. 不放物品i: 由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以被背包内的价值依然和前面相同。)
  2. 放物品i: 由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值。
    所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
 vector<int> weight = {1, 3, 4};
 vector<int> value = {15, 20, 30};
 int bagweight = 4;

    // 二维数组
    vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
    // 初始化
    for (int j = weight[0]; j <= bagweight; j++) {
        dp[0][j] = value[0];
    }
    // weight数组的大小 就是物品个数
    for(int i = 1; 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 - 1][j - weight[i]] + value[i]);
        }
    }

遍历顺序的解释

由于dp[i][j]是由dp[i - 1][j], dp[i - 1][j - weight[i]]得出来的,根据递推方向,所以i要从小到大,j要从小到大。
那么背包和物品的遍历顺序呢? 在二维数组中先遍历背包还是先遍历物品没有区别。
需要注意的是,二维数组需要考虑jj < weight[i]的情况(和上一轮数值相等,手动赋值),而一维数组直接沿用上一轮的数值,不必考虑。

一维动态数组(滚动数组)

dp[j]为容量为 j 的背包所背的最大价值。

dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])

此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值。
所以递归公式为:

dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

可以看出相对于二维dp数组的写法,就是把dp[i][j]中i的维度去掉了。
总体代码如下:

void test_1_wei_bag_problem() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagWeight = 4;

    // 初始化
    vector<int> dp(bagWeight + 1, 0);
    for(int i = 0; i < weight.size(); i++) { // 遍历物品
        for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
}

遍历顺序的解释

1.i、j大小的遍历顺序:
1.1 i的遍历顺序和二维一致
1.2 j的遍历顺序 一维数组和二维数组不一样的地方在于,二维数组中通过为dp[i-1][j-weight[i]来推导dp[i][j],在一维数组中没有i这个维度,而是重复利用一维数组的元素。 如果j从小到大遍历,当需要dp[i-1][j-weight[i]]时,用到的dp[j-weight[i]]已经不是i-1这一轮的数值了,而是被覆盖为i这一轮的数值,这样其实就是重复放了物品i !
例如:
物品0的重量weight[0] = 1,价值value[0] = 15

如果正序遍历

dp[1] = dp[1 - weight[0]] + value[0] = 15

dp[2] = dp[2 - weight[0]] + value[0] = 30

此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。

为什么倒序遍历,就可以保证物品只放入一次呢?

倒序就是先算dp[2]

dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)

dp[1] = dp[1 - weight[0]] + value[0] = 15

对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖!

2.背包和物品的遍历顺序:
再看看两个嵌套for循环的顺序,代码中是先遍历物品,嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?
不可以!
因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。例如求dp[4]时,j=4,i=0,需要由max(dp[4], dp[4 - weight[0]] + value[0]),即max(dp[4],dp[3] +value[0])由于先遍历的背包容量,因此此时的dp[3],dp[4]还没有求,仍然为初始值0,此时最大值必然为value[0],即每个dp[j]相当于只放了一个物品,因此不能先遍历背包容量。

总结

二维数组遍历顺序没有要求,但需要考虑j < weight[i]的情况
一维数组遍历背包时必须从大到小遍历,否则会导致重复选取物品; 必须先遍历物品,再遍历背包, 否则会导致每个dp[j]只放一个物品。一维数组不用考虑j < weight[i]的情况,直接沿用上一轮的情况。

完全背包有

N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
物品为:

重量价值
物品0115
物品1320
物品2430

其中每件商品都有无数个。问背包能背的物品最大价值是多少?

void test_CompletePack() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagWeight = 4;
    vector<int> dp(bagWeight + 1, 0);
    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]);
        }
    }
}

遍历顺序的解释

1.物品和背包的遍历顺序:
对于纯完全背包问题,先遍历物品再遍历背包和先遍历背包,后遍历物品都可以
2.i、j大小遍历顺序
01背包内嵌的循环 j(一维数组) 是从大到小遍历,为了保证每个物品仅被添加一次。而完全背包的物品是可以添加多次的,所以要从小到大去遍历

完全背包的排列组合问题

由于完全背包问题中的物品可以重复拿取,因此会产生拿取顺序的问题,组合问题即不考虑拿取先后顺序的问题,而排序问题需要考虑拿取的先后顺序。此外,如果求的是每个排列组合中的个数,则元素的顺序不重要,既可以有顺序,也可以没有顺序。

组合问题

如leecode518题,

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 :
输入: amount = 5, coins = [1, 2, 5] 输出: 4 解释: 有四种方式可以凑成总金额: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount+1,0);
        dp[0]=1;
        //如果先遍历背包就编程排列问题,而不是组合问题
        /*//以下为先遍历背包
        这会导致出现{1,2}与{2,1},而先遍历物品,物品i增加后不会回头重复添加
        for(int j=·;j<=amount;++j){//先遍历背包
            for(int i=0;i<coins.size();++i){//先遍历物品
                if(j>=coins[i]) dp[j]+=dp[j-coins[i]];
            }
        }
        */
        for(int i=0;i<coins.size();++i){//先遍历物品
            for(int j=coins[i];j<=amount;++j){//后遍历物品
                dp[j]+=dp[j-coins[i]];
            }
        }
        return dp[amount];
    }
};

排列问题

例如Leecode377.组合总和Ⅳ:
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
请注意,顺序不同的序列被视作不同的组合。

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<int> dp(target+1,0);
        dp[0]=1;
        for(int j=0;j<=target;++j){
            for(int i=0;i<nums.size();++i){
                if(j>=nums[i]&& dp[j] < INT_MAX - dp[j - nums[i]]){
                    dp[j]+=dp[j-nums[i]];
                }
            }
        }
        return dp[target];
    }
};

个数问题

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1: 输入:coins = [1, 2, 5], amount = 11 输出:3 解释:11 = 5 + 5 + 1

// 版本一
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 - coins[i]]是初始值则跳过
                    dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
                }
            }
        }
        if (dp[amount] == INT_MAX) return -1;
        return dp[amount];
    }
}
// 版本二
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount + 1, INT_MAX);
        dp[0] = 0;
        for (int i = 1; i <= amount; i++) {  // 遍历背包
            for (int j = 0; j < coins.size(); j++) { // 遍历物品
                if (i - coins[j] >= 0 && dp[i - coins[j]] != INT_MAX ) {
                    dp[i] = min(dp[i - coins[j]] + 1, dp[i]);
                }
            }
        }
        if (dp[amount] == INT_MAX) return -1;
        return dp[amount];
    }
}

可以看到,求排列组合中元素最小个数而非求排列组合的数量,此时先遍历背包还是先遍历物品都可以。

参考资料:
01背包
滚动数组
完全背包

  • 15
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Jaya算法并不是用于解决01背包问题的常见算法,而是一种基于群体搜索的优化算法。它通常用于参数优化、函数优化等问题。如果你想解决01背包问题,可以使用其他经典的算法,如动态规划。 动态规划是解决背包问题的常见方法之一。在01背包问题中,我们需要将一组物品放入容量有限的背包中,要求在不超过背包容量的前提下,能够使放入背包的物品总价值最大化。动态规划算法可以通过构建一个二维数组来解决该问题,其中行表示物品的选择顺序,列表示背包的容量。 具体的实现步骤如下: 1. 创建一个二维数组dp[][],其中dp[i][j]表示前i个物品在背包容量为j时的最大价值。 2. 初始化第一行和第一列为0,表示没有物品或背包容量为0时的最大价值都为0。 3. 对于每个物品i,遍背包容量j从0到总容量W: - 如果当前物品的重量wi大于背包容量j,则dp[i][j]等于上一个物品的最大价值dp[i-1][j]; - 如果当前物品的重量wi小于等于背包容量j,则dp[i][j]等于上一个物品的最大价值dp[i-1][j]和当前物品价值vi加上剩余空间容量j-wi的最大价值之间的较大值。 4. 最终的结果保存在dp[n][W]中,其中n表示物品数量,W表示背包总容量。 以上就是使用动态规划算法解决01背包问题的基本思路和步骤。希望对你有所帮助!如果有其他问题,请随时提问。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值