算法训练营(day43)
算法训练营(day43)
动态规划理论基础
动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。
举个例子:有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
动态规划中 dp[j]
是由 dp[j-weight[i]]
推导出来的,然后取 max(dp[j], dp[j - weight[i]] + value[i])
。
动态规划的解题步骤
- 确定
dp
数组(dp table)以及下标的含义 - 确定递推公式
dp
数组如何初始化- 确定遍历顺序
- 举例推导
dp
数组
01背包理论基础
01背包的主要出题方式是:有n
件物品和一个最多能背重量为 w
的背包。第 i
件物品的重量是 weight[i]
,得到的价值是 value[i]
。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
动规五部曲分析
确定dp数组(dp table)以及下标的含义
对于背包问题,有一种写法是使用二维数组,即**
dp[i][j]
表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少**。确定递推公式
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
得到的最大价值
- 不放物品i:由
所以递归公式为: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
;
dp数组的初始化
从
dp[i][j]
的定义出发:dp[i][0]
,即背包容量j为0,无论是选取哪些物品,背包价值总和一定为0dp[0][j]
,即存放物品编号i为0的时候,各个容量的背包所能存放的最大价值
确定遍历顺序
遍历的维度有两个:物品i 与 背包重量j
先遍历物品 还是 先遍历背包重量 其实都可以!! 但是先遍历物品更好理解。
推导dp数组
01背包测试代码
1 | public class BagProblem { |
滚动数组
滚动数组,就是把二维dp降为一维dp,我们重温一下:dp[i][j]
里的i和j表达的是i是物品,j是背包容量
- 在使用二维数组的时候,递推公式:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
- 如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:**
dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i])
;** - 再简化一下表达式,只用一个一维数组
dp[j]
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。
所以递归公式为:
1 | dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); |
滚动数组遍历背包顺序
二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。
倒序遍历是为了保证物品i只被放入一次!!!
一维dp遍历一定要 先物品后背包容量 吗?
答案:是的!!!
一维dp的写法中,背包容量一定是要倒序遍历的,如果遍历背包容量放在上一层,那么每个 dp[j]
就只会放入一个物品,即:背包里只放入了一个物品。
倒序遍历的原因,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。
(个人理解:一维dp就是先放大的,大的塞进去还有位置就再补小的,大的塞不进就换小一号的塞,保证自己不会犯糊涂,塞了两个一样小的充当一个大的用)
1049. 最后一块石头的重量 II
题目链接:https://leetcode.cn/problems/last-stone-weight-ii/description/
有一堆石头,用整数数组 stones
表示。其中 stones[i]
表示第 i
块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x
和 y
,且 x <= y
。那么粉碎的可能结果如下:
- 如果
x == y
,那么两块石头都会被完全粉碎; - 如果
x != y
,那么重量为x
的石头将会完全粉碎,而重量为y
的石头新重量为y-x
。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0
解题思路
解题过程:动态规划
按照动规五部曲来分析:
- 确定dp数组(dp table)以及下标的含义
**dp[j]表示容量为j的背包,最多可以背最大重量为dp[j]**。
- 确定递推公式
递归公式为: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
;
化简为一维数组的 01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
;
本题的递推公式为:**dp[j] = max(dp[j], dp[j - stones[i]] + stones[i])
**
dp数组的初始化
从
dp[i][j]
的定义出发:dp[i][0]
,即背包容量j为0,无论是选取哪些物品,背包价值总和一定为0dp[0][j]
,即存放物品编号i为0的时候,各个容量的背包所能存放的最大价值
本题dp的上限是 石头总重量的一半,因为重量不可能为负数,所以 dp的初始化定为0即可
- 确定遍历顺序
使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!
1 | for(int i = 0; i < len; i++){ |
- 推导dp数组
详细代码
1 | class Solution { |
494. 目标和
题目链接:https://leetcode.cn/problems/target-sum/description/
给你一个整数数组 nums
和一个整数 target
。
向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1]
,可以在2
之前添加'+'
,在1
之前添加'-'
,然后串联起来得到表达式"+2-1"
。
返回可以通过上述方法构造的、运算结果等于 target
的不同 表达式 的数目。
解题思路
解题过程:动态规划
按照动规五部曲来分析:
- 确定dp数组(dp table)以及下标的含义
对于背包问题,**dp[i][j]
表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少**。
本题的dp[j]
表示:填满 j
(包括j)这么大容积的包,有 dp[j]
种方法
- 确定递推公式
对于求组合类问题,动态规划的公式为:dp[j] += dp[j - nums[i]]
dp数组的初始化
从递推公式可以看出,对于求组合类问题在初始化的时候dp[0] 一定要初始化为1,因为dp[0]是在公式中一切递推结果的起源,如果dp[0]是0的话,递推结果将都是0。
确定遍历顺序
使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!
1 | for(int i = 0; i < len; i++){ |
- 推导dp数组
详细代码
1 | class Solution { |
474. 一和零
题目链接:https://leetcode.cn/problems/ones-and-zeroes/description/
给你一个二进制字符串数组 strs
和两个整数 m
和 n
。
请你找出并返回 strs
的最大子集的长度,该子集中 最多 有 m
个 0
和 n
个 1
。
如果 x
的所有元素也是 y
的元素,集合 x
是集合 y
的 子集 。
解题思路
解题过程:动态规划
按照动规五部曲来分析:
确定dp数组(dp table)以及下标的含义
dp[i][j]
:最多有i
个0和j
个1的strs的最大子集的大小为dp[i][j]
。确定递推公式
动态规划的递归公式为:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
;dp[i][j]
可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum
个0,oneNum
个1。dp[i][j]
就可以是dp[i - zeroNum][j - oneNum] + 1
。然后我们在遍历的过程中,取dp[i][j]
的最大值。得到本题的递推公式:
dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1)
;dp数组的初始化
从
dp[i][j]
的定义出发:dp[i][0]
,即背包容量j为0,无论是选取哪些物品,背包价值总和一定为0dp[0][j]
,即存放物品编号i为0的时候,各个容量的背包所能存放的最大价值
因为本题物品价值不会是负数,初始值只需设置为0,保证递推的时候
dp[i][j]
不会被初始值覆盖即可确定遍历顺序
本题物品就是strs里的字符串,背包容量就是题目描述中的m和n。
1 | for(String str : strs){ |
- 推导dp数组
详细代码
1 | class Solution { |