背包问题
完全背包是也是01背包稍作变化而来,即:完全背包的物品数量是无限的。
所以背包问题的理论基础重中之重是理解01背包问题。
01背包 二维dp数组
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
举个例子,背包最大重量为4。物品为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
问背包能背的物品最大价值是多少?
动规五部曲
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。物品为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
问背包能背的物品最大价值是多少?
动规五部曲
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 会报错?? 区间长度改的也都改了