背包问题 - 01背包&完全背包
问题定义
首先先定义下背包问题,方便后面讲述: 有N件物品,W容量的背包,第 i 件物品的重量为 weight[i],价值为 value[i],问在能装下的前提下如何使得背包的物品价值最大。
具体的例子如下,背包的容量为4。显然最大的价值是35。
0
1
15
1
2
20
2
4
30
核心代码
首先回顾下两个核心代码:
0/1背包的核心代码:
for (int i = 0; i < weight.size(); i++) {
for (int j = bagWeight; j >= weight[i]; j--) {
dp[j] = max(dp[j], d[j-weight[i]] + value[i]);
}
}完全背包的核心代码:
for (int i = 0; i < weight.size(); i++) {
for (int j = weight[i]; j <= bagWeght; j++) {
dp[j] = max(dp[j], dp[j-weight[i]] + value[i]);
}
}可以看到这两个问题的差别只有内层for循环的顺序不同:
为什么调整遍历的方向就可以?
按照最经典的例子,来看下每次更新的dp是怎样的,dp的大小应该是容量的大小+1,也就是5。这里要注意,很多时候我们是会多申请一个空间,dp[0]作为起始条件来推导使用。
对于0/1背包而言,当 i=0 遍历第一个物品时:
第一次遍历的过程:j=bagWeight=4,此时的条件是weight[i] = weight[0] = 1,dp[4]=max(dp[4], dp[4-1]+value[1]) = 15,结果如下:
第二次遍历的过程:j = 3,weight[i] = weight[0] = 1,dp[3] = max(dp[3], dp[3-1]+value[1]) = 15,结果如下:

遍历完第一个物品得到的结果为:

对于完全背包而言,当 i=0 遍历第一个物品时: 第一次遍历的过程:j=weight[i] = weight[0] = 1,条件是 bagWeight=4,dp[1] = max(dp[1], dp[1-1]+value[0]) = 15,结果如下:

第二次遍历的过程:j = 2,weight[i] = weight[0] = 1, dp[2] = max(dp[2], dp[2-1]+value[0]) = max(dp[2], dp[1]+value[0]) = 30,结果如下:

遍历完第一个物品得到的结果为:

回顾下在0/1背包问题和完全背包问题中dp的定义:
dp[j] 表示的是容量为 j 的背包所背物品的最大价值 -> 这个定义在两个问题中都是一致的。
通过上面的一个物品的遍历过程,可以看出两个遍历顺序带来的不同,在两个问题中是都需要利用之前的 dp 结果,也就是说dp[j] 需要用到dp[0...j-1]的结果,但是在完全背包问题中这些结果是已经计算过的,也就是说dp[0...j-1]已经放入了0个或多个物品了。而在0/1问题中,这些结果还没有计算,也就是没有放入物品的时候,每当计算到dp[i]的时候就只会放入一个物品。
应用举例
总结起来有三类问题,这里的核心代码根据题目会有一定的变通。遍历顺序只需要判断题目中"物品"能不能取多个来判断,多个就正向遍历,一个就逆向遍历。
解决问题的核心要点就是明确dp的定义,确定好初始化条件,然后明确遍历顺序,最后搞定!
其中遍历顺序上求的是组合数,就是外循环物品,内循环目标;如果是排列数,那就是外循环目标,内循环物品。
判断是否 核心代码
统计个数 核心代码
最值问题 核心代码
0/1背包
416. 分割等和子集 -> 判断是否
题目说明:将一个数组分割成两个和相同的子集,返回当前这个数组是否可以满足这个要求。
例子:[1, 5, 11, 5] 返回 true,因为可以分成 [1, 5, 5]和[11]
题目转化为0/1背包问题,就是在所有的数值中选,把数值当成物品,对应的数值大小就是value的大小,每个只能选一次,最终看能否选出和为所有数值总和一半的子集。 实现过程:
494. 目标和 -> 统计个数
题目说明:给定一个数组,可以在数值前面添上+号或者-号,构成target的方式多少种?
例子:[1, 1, 1],target = 1,那么就有三种构成方法,分别是 [-1, 1, 1] [1, -1, 1] [1, 1, -1]
这道题第一眼看起来可能会想到回溯方法来做,转化成背包问题需要一点技巧。
首先我们将数值的和分成两部分,这个时候是没有加符号的,两个部分和的大小由加入的数值决定。只是将两个数值和分成两部分。
现在给pos部分加上+号,neg部分加上-号,那么我们可以构建出以下等式:
消掉 pos后得到:
其中,sum和target都是已知的,那么neg也就是已知的,当然如果要转换成求pos也是可以的。
转化成背包问题就是从nums中选择数字,放进大小为neg的背包中。
474. 一和零 -> 最值问题
题目说明:给定一个0和1字符串的数组,找出满足m个0,n个1的最大子集长度。
举例说明:strs = ["10", "0", "1"], m = 1,n = 1。那么结果为1,因为满足的子集为["10"] 和 ["0"],["1"],所以最大长度为2。
这道题就比较不一样了,不完全是相同的0/1背包的套路了。这道题是两个维度的0/1背包了。
完全背包
518. 零钱兑换 II -> 统计个数
题目说明:给定金额大小,和一定数量的零钱,问构成零钱金额共有多少种方式,每种零钱可以取多个。
举例说明:amount = 5, coins = [1, 2, 5],那么共有四种方案构成总金额,5 = 5; 5 = 2 + 2 + 1;5 = 2 + 1 + 1 + 1;5 = 1 + 1 + 1 + 1 + 1;
所以思路也是比较明确的和494. 目标和是一样的,只不过按照前面的分析,0/1背包和完全背包的不同,只需要把内循环的逆向遍历改成正向遍历就可以了。
377. 组合总和 Ⅳ -> 统计个数
题目说明:和零钱兑换是一样的,只不过这里的构成方案有顺序要求。
举例说明:target = 4, nums = [1, 2, 3],构成target的共有7中方案:4 = 1 + 1 + 1 + 1; 4 = 1 + 1 + 2; 4 = 1 + 2 + 1; 4 = 2 + 1 + 1; 4 = 2 + 2; 4 = 1 + 2; 4 = 3 + 1;
可以看得出,这个时候组合的方式有了顺序的要求,也就是说[1, 2, 1] 和 [2, 1, 1]是不同的组合方式,而在零钱兑换这两种方式是一样的。
可以看到和零钱兑换II的遍历方式只是内外层循环调了个个而已,最终得到的一个是组合数,一个是排列数。怎么理解呢?
首先先假定nums=[1, 2, 3],target=4。
然后我们来看零钱兑换得到的组合数,外层循环遍历的是nums的内容,也就是说,每个nums[i]数值都只会进行遍历一次,也就是说对于某个target来说,只会选择一次该数值nums[i],所以最终得到的结果一定是按照nums[i]遍历顺序得到的结果,也就说会得到[1, 1, 2]的组合结果,而不会出现[2, 1, 1]的组合结果,因为外层遍历是从nums[0]到nums[2]遍历的。
相应的,在组合遍历的过程中,是将nums的遍历移到内层循环中去了。所以对于每个target来说,那么它都有可能取到nums[0]~nums[2]的数,也就是可能取到的是[1, 1, 2],也有可能取到的是[2, 1, 1]。
以上我们可以得到结论:
外循环物品,内循环目标 -> 组合数
外循环目标,内循环物品 -> 排列数
139. 单词拆分 -> 判断是否
题目说明:给定一个字符串和单词数组,问这个字符串是否可以被单词数组构成。
举例说明:s="applepenapple",wordDict=["apple", "pen"],显然是可以的。返回true的结果。
279. 完全平方数 -> 最值问题
题目说明:给定一个数值,问组成这个数的完全平方数的最少个数。其中1,4,9,16...是完全平方数
举例说明:n = 12 ,那么它可以等于 4 + 4 + 4,可以看到最少组成12的完全平方数的数量是3个。
这个题目也很好转换成背包问题,这里我们看到数值大小n可以看成背包容量,然后物品是完全平方数,每个数可以取多次也就是完全背包问题了。这样子代码就容易写出了。
参考文章
Last updated
Was this helpful?