动态规划之背包问题。
讲解背包问题最经典的当属《背包九讲》,之前看了前面一点点,现在又忘了,所以还是的写笔记
面试需要,不做太深入的了解。这里仅学习零一背包、完全背包和多重背包问题
[1] 参考了背包九讲 背包九讲就是讲背包的永远滴神
1 背包问题介绍
1.1 背包问题的基本描述和分类
背包问题的基本描述:有N件物品和一个最多能装重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。
- 01背包:每种物品仅有一件,可以选择放或不放。求解将哪些物品装入背包可使价值总和最大。
- 完全背包:每种物品都有无限件可用,求解将哪些物品装入背包可使价值总和最大。
- 多重背包:每件物品的数量由nums[i]表示,求解价值总和最大的装法。

1.2 背包问题的基本解法(01背包为例)
就是动态规划,但不同问题,遍历计算的顺序大不相同。
动态规划的三大要点:1状态 2确定初始化和重点 3列出状态转移方程
为什么要用动态规划?
一个物品存在选择或者不选择两种情况,暴力解法就是2^n种选择。显然需要用动态规划保存状态来降低时间复杂度。
基础的01背包案例:背包容量为n、一共有m个物品,物品重量数组W[1:m],价值数组V[1:m]
- 状态
dp[i][j]
表示从前i
个的物品种任意取,放进容量为j
的背包,能获得的最大价值总和 - 初始化状态和终点
-
dp[i][0]=0
(容量为0的背包取出的价值为0)dp[0][j]=0
(没有任何物品取出价值为0) dp[i][j]=dp[i-1][j]
(因为多一个物品一定比少一个物品获得可能的结果更大,所以可以用上一个状态表示初始值)- 终点:
dp[m][n]
一共有m个物品,背包容量为n
-
- 状态转移方程
- 若(j-W[i-1]>0),就表示W[i-1]元素可以作为填充背包的一部分,即
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-W[i-1]]+V[i-1]);
1 | public int backPack01(int m, int[] W, int[] V) { |
由遍历过程我们可以知道,dp[i][j]
层都是由dp[i-1][j]
层推导过来的,其实可以只使用一维数组保存状态。
要注意到,此时j
的循环必须是从大到小的逆序循环!! 因为dp[i][j]
是由dp[i-1][j]
dp[i-1][j-w[i-1]]
推导而来,,每次求j
的位置的值 用到了j
位置和j
之前位置的元素!!!
代码如下,不再赘述:
1 | public int backPack01(int m, int[] W, int[] V) { |
2 01背包题解
了解概念之后、直接刷题学习
0/1背包类型:5m、53e、55m、62m、70e、91m、121e、139m、152m、198m、 213m、300m、303e、309m、322m、338m、377m、416m、647m、673m、698m
416 1049 494 474
92 · 背包问题
在
n
个物品中挑选若干物品装入背包,最多能装多满?假设背包的大小为m
,每个物品的大小为A[i]
思路:dp[i][j]
表示使用前i
个物品最多能将容量为j
的背包装的容量大小
- 初始化
dp[i][0]=0 dp[0][j]=0
dp[i][j] = dp[i-1][j]
- 迭代方程
if(j>A[i-1]) dp[i][j] = dp[i-1][j-A[i-1]]+A[i-1]
代码:
1 | public class Solution { |
416. 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
思路:做这道题需要做一个等价转换:两子集元素和相等即表示:是否可以从输入数组中挑选出一些正整数,使得这些数的和 等于 整个数组元素的和的一半。
所以题目变成了,是否可以挑出一些数来是他们的和等于sum/2。挑选的过程就类似与背包选择的过程。经典的背包问题是能装多满、这里问的是是否刚好等于,所以状态数组dp[][]
是boolean类型的
- 状态
dp[i][j]
表示使用前i
个元素,是否存在一个组合使其和为j
- i从0到n-1,,j从0到target
- 初始化
dp[i][0]=false
;dp[0][j]=true(if nums[0]==j)
;dp[i][j] = dp[i - 1][j]
- 终点
dp[n-1][sum/2]
; - 动态迭代
if(j-nums[i]>=0 && dp[i-1][j-nums[i]]) dp[i][j] = true;
依然可以看到dp[i][j] = dp[i - 1][j]
类似的初始化,因为多一个元素一定比少一个元素的结果更理想,
然后动态迭代时,同样也用到了dp[i-1][j-nums[i]]
之前的状态,即使用nums[i]和不使用nums[i]存在明显的先后关系!!
代码:细节挺多的,不容易ak
1 | class Solution { |
494. 目标和
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-‘ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-‘ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
下意识看到觉得好难,然后思路是回溯….
方法一:回溯法(为什么不需要用for循环呢?)
1 | class Solution { |
方法二:递归
1 | class Solution { |
方法三:动态规划(01背包问题)
思路:看起来不像01背包、但其实就是01背包
dp[i][j]
表示前i个元素可以组成j值的组合数目- 动态规划问题,建议先考虑迭代方程(这样你才知道需要初始化哪些位置:因为不一定总是第1行第1列)
dp[i][j] = dp[i-1][j-nums[i]] + dp[i-1][j+nums[i]]
//总是由上一行推导这一行,所以初始化第一行就ok-
dp[0][j]
//第一个元素组合出j的种数,,那就是dp[0][nums[i]] += 1; dp[0][-nums[i]] += 1
; 就完事 !!注意用+=因为这两个可能是同一个位置!! - 最后因为
j
可能为负数,且范围是[-1000,1000], 所以j
索引的位置总是+1000,使索引非负
代码:
1 | class Solution { |
为什么说他是零一背包的问题呢?因为都是由上一行的状态推导这一行,并且推导过程需要减去(加上)nums[i]
dp[i][j] += dp[i-1][j-nums[i]]
dp[i][j] += dp[i-1][j+nums[i]]
474. 一和零
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
输入:strs = [“10”, “0001”, “111001”, “1”, “0”], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {“10”,”0001”,”1”,”0”} ,因此答案是 4 。
其他满足题意但较小的子集包括 {“0001”,”1”} 和 {“10”,”1”,”0”} 。{“111001”} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
思路:就是多了一维的01背包问题
dp[i][j][k]
表示前i
索引的字符串 满足0的个数<=j && 1的个数<=k的最大子集的大小dp[i][j][k] = dp[i-1][k][j]
;dp[i][j][k] = dp[i-1][j-a][k-b]+1
a、b分别表示strs[i]中0和1的个数- 初始化:01背包问题都是有上一行推导下一行、所以只用初始化第一行
dp[0][j][k] =1 if(j>=a&&k>=b)
代码:其实代码简单、就是不容易想到是01背包问题…
1 | class Solution { |
3 完全背包题解
518 377 322 279 139
70 爬楼梯是完全背包?我不会写了
440 · 背包问题 III
给定
n
种物品, 每种物品都有无限个. 第i
个物品的体积为A[i]
, 价值为V[i]
.再给定一个容量为
m
的背包. 问可以装入背包的最大价值是多少?样例 1:
1
2
3 输入: A = [2, 3, 5, 7], V = [1, 5, 2, 4], m = 10
输出: 15
解释: 装入三个物品 1 (A[1] = 3, V[1] = 5), 总价值 15.
思路一:先来一个01背包的解法。(01生万物)
- 完全背包问题,三层循环
-
dp[i][j]
表示前i
索引物品任意取,背包容量为j
时的最大价值 动态迭代 第`i`索引物品最多使用的个数是 `m/A[i]` 所以次数`k`取0到`m/A[i]`
- ``dp[i][j] = max(dp[i][j], dp[i-1][j-kA[i-1]]+kV[i-1])`
- 初始化
dp[0][j]=j/A[0]*A[0]
;dp[i][j]=dp[i-1][j]
终点dp[n-1][m]
代码:
1 | public class Solution { |
思考:
dp[i][j]=dp[i-1][j]
同样采用了多用一个元素一定比少用一个元素结果更理想的01背包思想!!动态迭代方程采用
dp[i-1][j-k*A[i]]+k*V[i]
,表示在上一行的选择基础上、选择k个A[i]元素的价值,,取其中的最大值就好了。就是多了一层k循环的01背包问题
思路二:对于时间问题的简化
01背包问题是每个物品只能选一次、所以我们只能在上一行未选择A[i]这个元素的基础上继续做选择(选或者不选),,,而完全背包的问题是A[i]这个元素可以选择任意次,所以我们可以直接在本行已选择A[i]的基础上继续选择!!!,这就是完全背包和01背包最根本的区别,所需的改动也仅仅是动态方程中从上行[i-1]
选还是从本行[i]
选
一旦我们从本行选择、就无需每次都遍历k次,直接减少一层循环!!因为本行的dp[i][j]
用到了本行的dp[i][j-A[i]]
所以,j
一定过要从小到大遍历!!
-
dp[i][j]
表示前i
索引物品任意取,背包容量为j
时的最大价值 - 动态迭代:
dp[i][j] = max(dp[i][j], dp[i][j-k*A[i-1]]+k*V[i-1])
- 注意因为
dp[i
][j]用到了dp[i][j-k*A[i-1]]
,所以j
的循环要从小到大 - 初始化
dp[0][j]=j/A[0]*A[0]
;dp[i][j]=dp[i-1
][j] 终点dp[n-1
][m]
代码:
1 | public class Solution { |
思路三:进一步对空间问题的简化
因为求本行的数据只用到了上一行j
位置的数据,和本行j
之前的数据,所以一维数组就可以(和01背包的空间优化是一个思路)
dp[j]
表示示前i
个物品任意取,背包容量为j
时的最大价值- 动态迭代 j从0迭代到m,,i还是从1迭代到n,,O(mn)时间复杂度
dp[j] = Math.max(dp[j], dp[j-A[i]]+V[i])
;- 初始化
dp[j]== j/A[0]*A[0]
; 终点dp[m]
代码:
1 | public class Solution { |
所以完全背包问题的时间空间简化版本、和01背包的空间优化版本,其实就只有一个j
遍历顺序的差别
518. 零钱兑换 II
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
思路:完全背包问题,注意第一行的初始化方式!!
dp[i][j]
表示前i
索引的硬币,能够拼成j
的组合数dp[i][j] += dp[i][j-coins[i]]
- 初始化
if(j%coins[0]==0) dp[0][j] = 1
,,,dp[i][j]= dp[i-1][j]
;
代码:
1 | class Solution { |
方法二:完全背包,优化空间
dp[j]
表示前i
索引的硬币,能够拼成j
的组合数dp[j] += [j-coins[i]]
- 初始化
if(j%coins[0]==0) dp[j] = 1
,,,dp[j]= dp[j]
代码:
1 | class Solution { |
4 总结
完全背包问题和01背包问题的区别在于是否可以重复选择,对应到代码层面,就是当前行是由当前行推导(完全背包)还是由上一行推导(01背包)