[日记]LeetCode算法·十九——动态规划④ 完全背包问题

文章详细阐述了完全背包问题的定义,与01背包问题的区别,并通过举例说明了遍历顺序对问题解决的影响。文中列举了多个LeetCode题目,如零钱兑换II、组合总和IV等,分析了它们与完全背包问题的关系,强调了遍历顺序选择(先物品后背包或先背包后物品)对于解决组合和排列问题的重要性。
摘要由CSDN通过智能技术生成

1 完全背包问题

完全背包问题也是背包问题的基础之一,与01背包的区别仅在于物体的数目无限。
完全背包问题:
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

在01背包问题中,使用一维dp数组时,我们从后向前遍历,从而保证了每个物品只能选一次,而在完全背包问题中,我们必须从前向后遍历,利用覆盖的特性,从而允许物品得到无限次的选取
在遍历顺序上:
1 先物品后背包:这种遍历顺序意味着组合,即顺序不重要,因此,每次都在只添加一类物品的情况下保证优化所有的背包容量。因为每次都只考虑一类物品,因此,每个容量下的物品必然是无序的,即{1,2,3}={3,2,1}。
2 先背包后物体:这意味着排列,顺序不同是有区别的。每一次都考虑在固定的背包容量下,从物品0-物品i的遍历,因此再考虑更高容量的背包时,依然会从头开始考虑物品0,这样的排序必然是有序的,即{1,2,3}!={3,1,2}。


代码如下:

int bagProblem(vector<int>& weight, vector<int>& value, int bagWeight)
{
    vector<int> dp(bagWeight + 1, 0);

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

2 零钱兑换II

LeetCode:零钱兑换II
这是一道完全背包的题目,背包的容量为amount,物体重量与价值一致,为coins[i],最终所求的是装满amount的方案有多少种。

由于找零结果是无序的,因此采用组合,先遍历物品(硬币),再遍历背包(金额)。

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        //一个完全背包问题,背包的容积为amount
        //物品的重量和价值一致,都为coins[i]
        //最终求的是装满amount大小的背包的方案有多少种

        vector<int> dp(amount+1,0);
        //先硬币种类,再遍历背包容积
        //递推公式为dp[j]=dp[j-num[0]]+dp[j-num[1]]+...+dp[j-num[?]]
        //初始化dp[0]=1
        dp[0]=1;
        for(int i=0;i<coins.size();++i)
        {
            for(int j=coins[i];j<=amount;++j)
            {
                dp[j]=dp[j]+dp[j-coins[i]];
            }
        }
        return dp[amount];
    }
};

3 组合总和Ⅳ

LeetCode:组合总和Ⅳ
和上一题唯一的区别在于,这一题所求的是排列先遍历背包,再遍历物品

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        //完全背包问题,容积为target的背包,装满target大小的背包的可能性有多少
        //递推公式为dp[j]=dp[j]+dp[j-nums[i]],且因为target>=1,所以dp[0]=1是可以放心初始化的
        //因为顺序不同的组合被认为是不同的组合,实际上就是一个排列问题
        //因此,最内层的循环理应每一次都从nums的最开头开始取数,防止遗漏
        //因此,先遍历容积,在遍历物品
        vector<int> dp(target+1,0);
        dp[0]=1;
        //由于每一次i都是从0开始遍历
        //所以{j,i}={1,3}和{j,i}={3,1}会被视为不同的组合被记录
        for(int j=1;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];
    }
};

4 爬楼梯

LeetCode:爬楼梯
曾经作为斐波那契数列的动态规划题目出现,如果每次能上1-m阶,那么就可以用完全背包问题解决,即背包=要上的阶数,物品=一次能上的阶数(1-m),因为爬楼梯必然是有序的,使用排列:先背包后物品

class Solution {
public:
    int climbStairs(int n) {
        int m=2;
        //使用背包问题去思考,相当于一个容积为n的背包
        //对重量和价值为1,2,...,m的物品求完全背包问题的解法个数
        //爬楼梯有先后顺序,是排列问题,需要最内层从0开始遍历物品
        //故先遍历背包
        vector<int> dp(n+1,0);
        dp[0]=1;
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=m;j++)
            {
                if(i>=j)
                    dp[i]+=dp[i-j];
            }
        }
        return dp[n];
    }
};

5 零钱兑换

LeetCode:零钱兑换
目标是用最少数量的物品装满amount大小的背包,遍历顺序无所谓,因为所求的是最小找零的个数。

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        //不需要找零的情况排除
        if(amount==0)
            return 0;
        
        //这是一个完全背包问题,在amount大小的背包里
        //用最少的物品装满
        vector<int> dp(amount+1,amount+1);
        //递推公式为dp[i]=min(1+dp[i-coin[0]],1+dp[i-coin[1]],...,)
        //所以dp[0]=0
        dp[0]=0;
        //先遍历背包
        for(int i=1;i<=amount;++i)
        {
            for(int j=0;j<coins.size();++j)
            {
                if(coins[j]<=i)
                    dp[i]=min(dp[i],1+dp[i-coins[j]]);
            }
        }

        if(dp[amount]==amount+1)return -1;
        else return dp[amount];
    }
};

6 完全平方数

LeetCode:完全平方数
做了上道题和这道题,才发现以前好多自然而然解决的动态规划题,本质都是背包问题。

class Solution {
public:
    int numSquares(int n) {
        //完全背包问题,用1,4,9,...的物品去装容积为n的背包
        vector<int> dp(n+1,n+1);
        //递推公式dp[i]=min(dp[i],1+dp[i-num[j]*num[j]])
        dp[0]=0;
        for(int i=1;i<=n;++i)
        {
            for(int j=1;j*j<=i;++j)
            {
                dp[i]=min(dp[i],1+dp[i-j*j]);
            }
        }
        return dp[n];
    }
};

7 单词拆分

LeetCode:单词拆分
因为可以重复使用,所以是完全背包问题,因为单词的拼写是有序的,applepen!=penapple,所以必须使用排列:先遍历背包再遍历物品

class Solution {
public:
    bool suffixSame(string& s,int s_length,string& word)
    {
        for(int i=0;i<word.size();++i)
        {
            if(word[word.size()-1-i]!=s[s_length-1-i])
                return false;
        }
        return true;
    }
    bool wordBreak(string s, vector<string>& wordDict) {
        //一个完全背包问题,背包容量为s,物品为wordDict
        vector<bool> dp(s.size()+1,false);
        dp[0]=true;
        //必须先遍历背包,再遍历物品,这是因为本题讲究的是排列
        //applepen=apple+pen!=pen+apple
        for(int i=1;i<=s.size();++i)
        {
            for(int j=0;j<wordDict.size();++j)
            {
                string word=wordDict[j];
                if(i<word.size())
                    continue;
                else
                {
                    //背包大小为i,word大小为w.size,前置单词数为i-w.size,起点为i-w.size
                    if(suffixSame(s,i,word) && dp[i]==false)
                        dp[i]=dp[i-word.size()];
                }
            }
        }
        return dp[s.size()];
    }
};

8 总结

完全背包问题的精髓在于,遍历顺序的选择,无论是从前到后的允许多次,还是先背包或先物品导致的排列与组合,关键在于对问题进行抽象
——2023.3.4

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值