动态规划——完全背包的学习与思考

249 阅读6分钟

完全背包问题的学习与思考

01背包设置一维dp数组遍历顺序的讨论

在看这一篇文章之前,建议先看我的另一篇文章 01背包的学习与思考

01背包的学习与思考 这篇文章中,针对状态压缩,即在讨论如何将dp数组设置为一维数组? 的时候,实际上有提到,遍历顺序为先遍历物品再遍历背包的时候,针对01背包问题,我们在遍历背包时(也就是遍历一维dp数组时),要求下标从大到小进行遍历。

在01背包文章中也已经提到,下标从大到小进行遍历,根据其递推公式dp[j]=Math.max(dp[j],dp[j-curWeight]+curValue); 这里面dp[j-curWeight]指向的一定是下图中的黄色部分,即“历史”,而如果下标从小到大进行遍历,下标j-curWeight指向的实际上是前置的蓝色,如下图的第二部分,可能在j=0的时候,已经放入了物品0,而在j=1的时候,又会继续放入物品0,这就违背了01背包的每个物品只能被放入一次。

一维dp遍历顺序.png

完全背包问题的概念

但是,我们这里讨论的是完全背包问题。

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

这意味着什么?这意味着,正好套上了前面01背包问题,创建一维dp数组,但是遍历顺序是从小到大进行遍历的特点。背包大小从小到大进行遍历,对于01背包问题而言是违反了 01的概念,但是却恰好契合了完全背包的概念。

这里我们仍然还是采取01背包问题中的例子,如下表所示:(背包的最大容量是4)

重量价值
物品0115
物品1327
物品2434

那么我们手动来比划,针对完全背包,物品0~物品2都可以无限重复取,最大价值如下:

可以很容易地推导到,最大的价值实际上是不断地、反复地去放物品0。

完全背包示例1.png

完全背包问题的组合和排列问题

在完全背包的应用问题中,实际上还区分为排列问题和组合问题。

这里我们直接说结论:

  • 完全背包求组合数,那么就要先遍历物品,再遍历背包
  • 完全背包求排列数,那么就要先遍历背包,再遍历物品。

关于组合和排列的说明:

比如说我们现在有面额为1和2的硬币,数量有无数个,那么凑成面额为5元的方案有多少种?

其中一定存在这两个情况: 1、2、2 和 2、1、2 。

对于组合来说,这俩就是一个方案,因为组合 不考虑顺序问题。

对于排列来说,这俩就是两个方案,因为排列,要考虑顺序问题。

这里抛出一个疑问,为什么呢? 为什么偏偏先遍历物品再遍历背包 就是 求解组合数;先遍历背包再遍历物品,就是求解排列数呢?

这里实际上仍然举个例子就行了,我们简化一下上面的示例,比如我们现在手里有面额为1和2的硬币,凑成面额为3元

  • 显而易见,无法两种凑合方式 1、2 和 2、1
  • 凑成面额为3的组合方式,结果为1,因为1、2和2、1是一样的,顺序无关
  • 凑成面额为3的排列方式,结果为2,因为1、2和2、1的顺序不一样。
  1. 先遍历物品再遍历背包,只能是组合,而不能是排列。

    • 物品只有1和2,背包就是一维dp数组,背包容量从0~3,那么想象一下,先遍历物品再遍历背包,当遍历到物品为1时,会用这个1把整个dp数组都刷一遍,然后遍历到物品2时,会再用这个2把整个dp数组再刷一遍,最后得到结果。
    • 那么!这样的话,就只有1、2 何来的2、1 ?!!! 所以只能解决组合问题。
  2. 先遍历背包再遍历物品,解决的是排列问题

    • 先遍历背包,比方说现在遍历背包容量为1时,此时针对这个容量,再遍历物品,会用1,2都刷一遍;然后到了背包容量为2,此时针对容量2,又用物品1,2再刷一遍。这时候物品1、2和物品2、1顺序都兼顾到了

题目实例:

组合问题:518.零钱兑换II

这道题里面显式地提出了返回可以凑成总金额的硬币组合数

  1. 确定dp数组及其含义: int[]dp=new int[amount+1];

    • dp[j]的含义为在总金额为j的情况下,能够通过硬币凑成金额为j的硬币组合数
  2. 确定递推公式:

    • 当总金额j>= coins[i]的情况下,即当前硬币的面额能够加入到总金额j的组成之后,构成总金额j,能够有几种方法。
    • 当总金额j<coins[i]的情况下,即当前硬币的面额无法加入到总金额j的组成之中,保持现状dp[j]=dp[j]即可
  3. dp数组初始化:

    • 这里特别说明一下为什么要设置dp[0]=1
    • 实际上也非常简单,比方说当前要凑成的总金额为2,然后手里面的硬币面额为2,肯定是能凑成的,确确实实是一种方法,那么dp[2-2]dp[0] 一定要是1
class Solution {
    public int change(int amount, int[] coins) {
        int[] dp=new int[amount+1];
        dp[0]=1;
        for(int i=0;i<coins.length;i++){
            for(int j=1;j<=amount;j++){
                if(j>=coins[i]){
                    dp[j]=dp[j]+dp[j-coins[i]];
                }
            }
        }
        return dp[amount];
    }
}

排列问题377. 组合总和 Ⅳ

这道题实际上名字取的有点问题,说是组合总和,但是实际上看其示例,本质上求得是排列问题

class Solution {
    public int combinationSum4(int[] nums, int target) {
        int[]dp=new int[target+1];
        dp[0]=1;
        for(int j=0;j<=target;j++){
            for(int i=0;i<nums.length;i++){
                if(j<nums[i]){
                    continue;
                }
                else{
                    dp[j]+=dp[j-nums[i]];
                }
            }
        }
        return dp[target];
    }
}