动态规划3
完全背包
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
代码上两者最大的不同就是遍历顺序
一维数组的01背包要求先遍历物品,再遍历容量并且要按照从大到小的顺序进行容量遍历。这样可以保证前边数据在使用dp[i-j]时使用的是上一阶段数据,也就是dp[i]和dp[i-j]都是上一轮的而不是在这一轮实时更新的
而完全背包就不一样我就是可以多次的添加,所以把倒序变了回来。
1 | // 先遍历物品,再遍历背包 |
在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!因为没有了倒序所以就无所谓了。
因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。
遍历物品在外层循环,遍历背包容量在内层循环,状态如图:
遍历背包容量在外层循环,遍历物品在内层循环,状态如图:
518.零钱兑换II
面额的数目无限,那么就是完全背包,并且要求有多少种方案,那么就是一个组合的问题,当然可以通过回溯的方法搜索但是,太大了,所以可以通过dp解决
直接动态规划五部曲
- 确定dp数组以及下标的含义
意义就是对于j的钱最多有dp[j]种方法凑齐
- 递推公式
对于每一个钱都有取和不取的情况,所以在总金额j的时候就是需要把所有金额的可能情况算一遍比如j=5 硬币为123,我们遍历就是对拿了1的时候dp[4]有多少种情况,拿了2dp[3]有多少种情况,所以递推公式为dp[j] += dp[j - coins[i]]
- dp数组如何初始化
首先我们得保证最开始不为0 ,那么最开始为多少合适呢可以从dp[1]开始看,如果dp[1]=1那么不就是dp[0]=1
- 确定遍历顺序
因为没有了从后遍历那么先遍历背包还是物品就变得随意了,但是我们的题目要求的是凑成总和的组合数,元素之间明确要求没有顺序。所以纯完全背包是能凑成总和就行,不用管怎么凑的。本题是求凑出来的方案个数,且每个方案个数是为组合数。那么本题,两个for循环的先后顺序可就有说法了。
我们先来看 外层for循环遍历物品(钱币),内层for遍历背包(金钱总额)的情况。
1 | for (int i = 0; i < coins.size(); i++) { // 遍历物品 |
假设:coins[0] = 1,coins[1] = 5。
然而此时是先遍历的物品所以物品只使用一次
物品只有coins[0] 时,dp[6]时使用了1,那么dp[5]有多少方案1中111111
然后当dp[6]使用到5时就才使一个完全体
那么就是先把1加入计算,然后再把5加入计算,得到的方法数量只有{1, 5}这种情况。而不会出现{5, 1}的情况。
所以这种遍历顺序中dp[j]里计算的是组合数!
如果把两个for交换顺序,代码如下:
1 | for (int j = 0; j <= amount; j++) { // 遍历背包容量 |
我们来分析dp[6]因为遍历了物品如果使用了1那么dp[5]有多少方案,如果使用了5那么dp[1]有多少方案
背包容量的每一个值,都是经过 1 和 5 的计算,包含了{1, 5} 和 {5, 1}两种情况。
此时dp[j]里算出来的就是排列数!
记住先遍历物品组合后遍历物品排列。
1 | class Solution { |
70. 爬楼梯(进阶版)
每次你可以爬至多m (1 <= m < n)个台阶
每次你可以取1-m任意的数
所以就是完全背包
- 确定dp数组以及下标的含义
dp[i]:爬到有i个台阶的楼顶,有dp[i]种方法。
- 确定递推公式
dp[i] += dp[i - j]
- dp数组如何初始化
这种累加一般都初始化为1
- 确定遍历顺序
这是求得排列问题所以后遍历物品
一些说明:
i=1就是?都可以
j=1是最少一层
1 |
|
322. 零钱兑换
- 递推公式
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
- dp数组如何初始化
考虑到递推公式的特性,dp[j]必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。
- 确定遍历顺序
本题求钱币最小个数,那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数。
所以本题并不强调集合是组合还是排列。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
1 | class Solution { |
279.完全平方数
- 确定dp数组(dp table)以及下标的含义
dp[j]:和为j的完全平方数的最少数量为dp[j]
- 确定递推公式
dp[j] 可以由dp[j - i * i]推出, dp[j - i * i] + 1 便可以凑成dp[j]。
- 初始化
所以非0下标的dp[j]一定要初始为最大值,这样dp[j]在递推的时候才不会被初始值覆盖。
- 确定遍历顺序
1 | class Solution { |
139.单词拆分
回溯算法:分割回文串 (opens new window):是枚举分割后的所有子串,判断是否回文。
本道是枚举分割所有字符串,判断是否在字典里出现过。
那么这里我也给出回溯法C++代码:
1 | class Solution { |
- 时间复杂度:O(2^n),因为每一个单词都有两个状态,切割和不切割
- 空间复杂度:O(n),算法递归系统调用栈的空间
但是超时
背包问题
单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。
确定dp数组以及下标的含义
dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。
确定递推公式
如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。
所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。
- dp数组如何初始化
从递推公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递推的根基,dp[0]一定要为true,否则递推下去后面都都是false了。
- 确定遍历顺序
题目中说是拆分为一个或多个在字典中出现的单词,所以这是完全背包。
还要讨论两层for循环的前后顺序。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
而本题其实我们求的是排列数,为什么呢。 拿 s = “applepenapple”, wordDict = [“apple”, “pen”] 举例。
“apple”, “pen” 是物品,那么我们要求 物品的组合一定是 “apple” + “pen” + “apple” 才能组成 “applepenapple”。
“apple” + “apple” + “pen” 或者 “pen” + “apple” + “apple” 是不可以的,那么我们就是强调物品之间顺序。
所以说,本题一定是 先遍历背包,再遍历物品。
1 | class Solution { |
难点1在于想到用背包解决:单词s能不能被完全找到
难点2一定要先遍历背包
难点3遍历物品时把当前容量拆开看看能不能被填满也就是for (int j = 0; j < i; j++)难想
拓展:关于遍历顺序,再给大家讲一下为什么 先遍历物品再遍历背包不行。
这里可以给出先遍历物品再遍历背包的代码:
1 | class Solution { |
使用用例:s = “applepenapple”, wordDict = [“apple”, “pen”],对应的dp数组状态如下:
最后dp[s.size()] = 0 即 dp[13] = 0 ,而不是1,因为先用 “apple” 去遍历的时候,dp[8]并没有被赋值为1 (还没用”pen”),所以 dp[13]也不能变成1。
除非是先用 “apple” 遍历一遍,再用 “pen” 遍历,此时 dp[8]已经是1,最后再用 “apple” 去遍历,dp[13]才能是1。
多重背包代码
1 |
|
背包问题总结
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
其实这五部里哪一步都很关键,但确定递推公式和确定遍历顺序都具有规律性和代表性,所以下面我从这两点来对背包问题做一做
背包递推公式
问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下:
问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下:
- 动态规划:494.目标和(opens new window)
- 动态规划:518. 零钱兑换 II(opens new window)
- 动态规划:377.组合总和Ⅳ(opens new window)
- 动态规划:70. 爬楼梯进阶版(完全背包)(opens new window)
问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); ,对应题目如下:
问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); ,对应题目如下:
遍历顺序
01背包
在动态规划:关于01背包问题,你该了解这些! (opens new window)中我们讲解二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
和动态规划:关于01背包问题,你该了解这些!(滚动数组) (opens new window)中,我们讲解一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。
一维dp数组的背包在遍历顺序上和二维dp数组实现的01背包其实是有很大差异的,大家需要注意!
完全背包
说完01背包,再看看完全背包。
在动态规划:关于完全背包,你该了解这些! (opens new window)中,讲解了纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
但是仅仅是纯完全背包的遍历顺序是这样的,题目稍有变化,两个for循环的先后顺序就不一样了。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
相关题目如下:
- 求组合数:动态规划:518.零钱兑换II(opens new window)
- 求排列数:动态规划:377. 组合总和 Ⅳ (opens new window)、动态规划:70. 爬楼梯进阶版(完全背包)(opens new window)
如果求最小数,那么两层for循环的先后顺序就无所谓了,相关题目如下:
对于背包问题,其实递推公式算是容易的,难是难在遍历顺序上,如果把遍历顺序搞透,才算是真正理解了。
总结总结
这篇背包问题总结篇是对背包问题的高度概括,讲最关键的两部:递推公式和遍历顺序,结合力扣上的题目全都抽象出来了。
而且每一个点,我都给出了对应的力扣题目。
最后如果你想了解多重背包,可以看这篇动态规划:关于多重背包,你该了解这些! (opens new window),力扣上还没有多重背包的题目,也不是面试考察的重点。
如果把我本篇总结出来的内容都掌握的话,可以说对背包问题理解的就很深刻了,用来对付面试中的背包问题绰绰有余!