第十五天打卡——背包问题

背包问题

完全背包是也是01背包稍作变化而来,即:完全背包的物品数量是无限的。 

所以背包问题的理论基础重中之重是理解01背包问题。

01背包 二维dp数组

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

举个例子,背包最大重量为4。物品为:

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

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

动规五部曲

 1、明确dp数组的含义和下标   重点

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


2、确定递推公式   难点及关键点!!!

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

        不放物品i:由dp[i - 1][ j ]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)

        放物品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]);


3、初始化dp数组      重点
  • 首先初始化第一列:背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。
  • 根据递推公式,可以看出 i 是由 i-1 推导出来,需要初始化第一行:dp[0][j],即 i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。 j < weight[0]的时候,dp[0][j] = 0,当j >= weight[0]时,dp[0][j] = value[0]。
  • 初始化剩余值:其他下标初始化需要是最小值,后续会被max覆盖。如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。

4、明确遍历顺序  

如图可以看出,dp数组有两个遍历的维度:物品与背包重量。 

根据递推公式,从上图中可以看出,dp[i][j]的数值,一个来源于它的上方,一个来自于它的左上方(不一定是邻近的)。

根据循环遍历,无论是先遍历物品还是先遍历背包重量,都会在[i][j]之前先把它的上方和左上方数值遍历到。所以无论先遍历哪个都可以。先遍历物品会更好理解些(先遍历第一行)


5、打印dp数组(debug验证用)

01背包 滚动数组  

滚动数组就是把二维dp数组降为一维dp数组。

同个例子,背包最大重量为4。物品为:

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

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

动规五部曲

 1、明确dp数组的含义和下标   重点

        dp[ j ]:容量为j的背包,所背的物品价值总和最大是多少

2、确定递推公式   难点及关键点!!!

       dp[j]同样可以通过两个选择推出来

        不放物品i:由dp[ j ]推出,即背包容量为j,里面不放物品i的最大价值。此时dp[j]就相当于二维数组中的dp[i - 1][j]。(不放物品 i 就相当于不覆盖原来的数据,就是把上一层的数据拷贝下来

        放物品i:取dp[ j - weight[i] ] + value[ i ]

所以递归公式: dp[j] = max(dp[j], dp[ j - weight[i] ] + value[i]);

3、初始化dp数组    

dp[0] = 0;

非0下标也初始化0,因为后续还需要被覆盖掉。

4、明确遍历顺序   重点
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]);
    }
}
  • 倒序遍历

二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。倒序遍历是为了保证物品 i 只被放入一次,但如果一旦正序遍历了,那么物品0就会被重复加入多次。

  •  先物品后背包 

两个for循环的顺序也不能颠倒先遍历物品再遍历背包容量如果反过来,会导致dp数组记录的都是一个物品的数值了,就变成了重量j的背包能够放的物品的最大价值。

5、打印dp数组(debug验证用)

416. 分割等和子集 

本题可以利用回溯法暴力解决,但容易超时。可以用回溯法,尝试解决如下两题:

698.划分为k个相等的子集   (待刷)

473.火柴拼正方形  (待刷)

01背包思路:确定4点

  • 背包的体积为sum / 2
  • 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为 元素的数值
  • 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
  • 背包中每一个元素是不可重复放入

以上分析完,我们就可以套用01背包,来解决这个问题了。

动规五部曲:

1、明确dp数组的含义和下标   

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

2、确定递推公式  

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

3、初始化dp数组     

        dp[0] = 0;  如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。

4、明确遍历顺序

        1、倒序遍历   2、先物品后背包

5、打印dp数组(debug验证用)


1049. 最后一块石头的重量 II 

本题其实就是尽量让石头分成重量相同的两堆相撞之后剩下的石头最小。

怎么自己想不出来呢!!!

思路有了,后续实现是常规的背包问题。

实现过程中遇到的问题,不知道返回的结果怎么表示。答:

dp[n]里是容量为n的背包所能背的最大重量。n = sum / 2

那么分成两堆石头,一堆石头的总重量是dp[ n ],另一堆就是sum - dp[ n ]。

在计算n的时候,n = sum / 2 因为是向下取整,所以sum - dp[n] 一定是大于等于dp[n]的

那么相撞之后剩下的最小石头重量就是 ( sum - dp[ n ] ) - dp[ n ]。

本题其实和416. 分割等和子集 (opens new window)几乎是一样的,只是最后对dp[target]的处理方式不同。上一题相当于是求背包是否正好装满,而本题是求背包最多能装多少。


494. 目标和 

思路很重要!也很奇妙 

对nums数中的元素或作加法、或作减法,使其目标值等于target(S)。从整体来看就是,令加法的总和 - 减法的总和 = target。假设作加法的总和为x,作减法的总和为sum-x:

        target = x - (sum - x)  —> x = (target + sum) / 2;

tip:如果(target + sum) / 2,不能整除,则说明是无解的。

所以问题就转换成了装满容量为x的背包,有几种方法。

动规五部曲:

1、明确dp数组的含义和下标   

        dp[j]:装满容量为 j 的背包,有 dp[j] 种方法。

2、确定递推公式   关键!!!

        dp[j] += dp[j - nums[i]]  (所有利用背包求排列组合问题的公式,都是类似这种)

只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。

例如:dp[j],j 为5,

  • 已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包。
  • 已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包。
  • 已经有一个3(nums[i]) 的话,有 dp[2]中方法 凑成 容量为5的背包
  • 已经有一个4(nums[i]) 的话,有 dp[1]中方法 凑成 容量为5的背包
  • 已经有一个5 (nums[i])的话,有 dp[0]中方法 凑成 容量为5的背包

那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来

3、初始化dp数组     

        根据递推公式,可以得出dp[0] = 1。

4、明确遍历顺序

        1、内循环倒序遍历   2、先物品( nums[i] )后背包( x )

5、打印dp数组(debug验证用)


474.一和零  

本题是含有两个维度01背包问题。

动规五部曲:

1、明确dp数组的含义和下标   

        dp[i][j]:最多由 i 个0,j 个1的条件下,可装入的strs最大子集的长度

2、确定递推公式    关键!!!

        dp[i][j] = max(dp[i][j],  dp[i - zeroNum][j - oneNum] + 1);

tips:zeroNum,oneNum是当前遍历到的数组中某个字符串的0的个数和1的个数。

如果类比max(dp[j], dp[ j - weight[i] ] + value[i]),zeroNum和oneNum就相当于两个维度的weight[i],+1相当于+value[i]。

3、初始化dp数组     

        dp[0] = 0;  

4、明确遍历顺序

        1、倒序遍历   2、先物品(strs)后背包(m,n)

5、打印dp数组(debug验证用)


完全背包

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件

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

01背包和完全背包唯一不同就是体现在遍历顺序

 动规五部曲

 1、明确dp数组的含义和下标   

        dp[ j ]:容量为j的背包,所背的物品价值总和最大是多少

2、确定递推公式   

       与01背包相同: dp[j] = max(dp[j], dp[ j - weight[i] ] + value[i]);

3、初始化dp数组    

        dp[0] = 0;非0下标也初始化0,因为后续还需要被覆盖掉。

4、明确遍历顺序     关键点以及区别处!!!
  • 内循环正序遍历

倒序遍历就是为了保证物品 i 只被放入一次如果改为正序遍历了,那么物品0就会被重复加入多次。

完全背包不限制物品的个数,允许被重复加入多次,所以需要改为正序遍历。

  • 纯背包问题中物品和背包循环嵌套顺序无所谓

在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!

因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。

5、打印dp数组(debug验证用)

518. 零钱兑换 II 

本题的递推公式,与494. 目标和 (opens new window)中的递推公式是一致的,都属于组合问题而本题的难点在于遍历顺序。

在求装满背包有几种方案的时候,认清遍历顺序是非常关键的。

如果求组合数就是外层for循环遍历物品,内层for遍历背包。  先遍历行 

for (int i = 0; i < coins.size(); i++) { // 遍历物品
    for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
        dp[j] += dp[j - coins[i]];
    }
}

如果求排列数就是外层for遍历背包,内层for循环遍历物品先遍历列 

for (int j = 0; j <= amount; j++) { // 遍历背包容量
    for (int i = 0; i < coins.size(); i++) { // 遍历物品
        if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
    }
}


377. 组合总和 Ⅳ  

本题属于排列问题。明确递推遍历顺序:先背包后物品

假设:coins[0] = 1,coins[1] = 5。

背包容量的每一个值,都是经过 1 和 5 的计算,包含了{1, 5} 和 {5, 1}两种情况。

此时dp[j]里算出来的就是排列数


70. 爬楼梯 (进阶)

改为:一步可以一个台阶,两个台阶,三个台阶,.......,直到 m个台阶。问有多少种不同的方法可以爬到楼顶呢?每一阶可以重复使用,例如跳了1阶,还可以继续跳1阶。

马上从斐波那契数列问题变成求排列数的完全背包问题。


322. 零钱兑换 

动态规划五部曲

1、明确dp数组的含义和下标   

        dp[ j ]:凑成为 j 的总金额,所需的最少硬币个数为dp[ j ]

2、确定递推公式    

       dp[j] = min(dp[j], dp[ j - coins[i] ] + 1);   // 只有dp[ j - coins[ i ] ]不是初始最大值时,该位才有选择的必要。

if (dp[j - coins[i]] != Integer.MAX_VALUE){
    dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
}

3、初始化dp数组    

        dp[0] = 0;其余值初始化为Integer.MAX_VALUE

4、明确遍历顺序 

       1、正序  2、先物品后背包


279.完全平方数 

思路同上题。

代码实现中需要注意的地方就是完全平方数的遍历,问题不大,一刷ac。


139.单词拆分 

这个题有点绕,二刷的时候注意些。

动规五部曲

1、明确dp数组的含义和下标   

        dp[i] : 字符串长度为 i 的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词

2、确定递推公式    

        j < i,如果 [ j ~ i-1] 区间的单词在字符串中出现过,且dp[ j ]为true,则dp[ i ]为true

3、初始化dp数组    

        dp[0] = true;其余值初始化为false。

4、明确遍历顺序 

        求排列数(字符串前后顺序不能变换),先背包后物品

问题:为什么dp数组的含义把长度为i,变成下标为 i 会报错?? 区间长度改的也都改了


多重背包

总结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值