如果某一个问题有很多重叠子问题,使用dp是最有效的。
动态规划中每个状态一定由上一个状态推导而来。
动规五部曲:
确定dp数组以及下标的含义;
确定递推公式;
dp数组的初始化;
确定遍历顺序;
举例验证dp数组
基础问题
选择从下标0或下标1开始爬楼梯,也就是说刚开始不产生花费,dp_0=0,dp_1=0。其余部分与爬楼梯类似。
m*n的网格,与爬楼梯类似,可以用一维的滚动数组进行空间复杂度上优化
m*n的网格,有阻碍,在初始化时注意阻碍只要出现,从它开始后面就都得是0了;
还有一个测试用例是只有一格,刚开始就有阻碍。。。
有个结论,尽可能的拆成3,可以让乘积最大(最后剩余4的时候不再拆成3和1)
以n=3为例,当主节点为1时,左孩子无,右孩子有两个节点,于是为dp[0]*dp[2];
当主节点为2时,左右孩子都只有一个节点,dp[1]*dp[1];
当主节点为3时,右孩子无,左孩子有两个节点,于是dp[2]*dp[0]。最后将这些相加便是dp[3]。
递推关系式为:
dp[i]+=dp[以j为头节点左子树节点数]*dp[以j为头节点右子树节点数]。
j相当于头节点元素,从1遍历到i
dp[i]+=dp[j-1]*dp[i-j]
背包问题
0-1背包问题
有n个物品和最多能背重量为w的背包,第i件物品的重量是weight[i],价值是value[i],每件物品只能使用一次,求解哪些物品放入背包价值最大。
例如:
确定dp数组以及下标的含义
dp[i][j]表示从下标0-i的物品里任意取,放进容量为j的背包,价值总和最大为dp[i][j]
确定递推关系式
可以从两个方向推导出dp[i][j]
不放物品i 背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]
放物品i d p[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,
那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值
dp数组的初始化
背包容量为0时,一定为0;由递推关系可知i状态的价值由i-1状态推导而来,所以物品0的状态都需要初始化,即dp[0][j]
确定遍历顺序 先物品再背包
举例验证dp数组
滚动数组
确定dp数组的含义及其下标 dp[j]表示容量为j的背包,所背物品价值最大dp[j]
一维dp数组的递推公式 dp[j]=max(dp[j],dp[j-weight[i]]+value[i])
一维数组的初始化
一维dp数组的遍历顺序 先物品后背包 背包倒序
举例验证dp数组
将数组分割成相等的两个子集,也就是说在数组中任意取如果能取到和为sum/2,则表示可以分。
于是题目转化为:能否在子集中找到和为sum/2的组合。
递推关系式:dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]);
dp[j]表示背包总容量为j时,此时的和为dp[j],如果dp[sum/2]==sum/2的话,就表示可以找到。
将石头尽可能的分成两堆相等的数组,这样相减后才最小。
最后dp[target]里是容量为target的背包所能背的最大重量,那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target]。在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的。那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]。
left+right=sum
left-right=target,那么left=(sum+target)/2。问题转为装满背包容量为left的背包有多少种装法。
递推关系式:
在已有nums[i]的情况下,有dp[j-nums[i]]种方法凑成dp[j]
递推关系式:dp[j]+=dp[j-nums[i]]。
strs相当于物品,m个0与n个1表示两个维度的背包容量。
dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j];
递推关系式:dp[i][j]=max(dp[i][j],dp[i-zeroNum][j-oneNum]+1)。
完全背包问题
有N个物品和最多能被重量为w的背包,每个物品都有无限个(可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
01背包和完全背包的唯一不同就体现在:遍历顺序上 先物品后背包 背包正序
完全背包的装满背包最多有多少种方法问题。
求的是组合个数,于是在遍历顺序上先物品后背包。
求排列的话,在遍历顺序上先背包后物品。
改为:一步一个台阶,两个台阶,三个台阶,.......,直到 m个台阶。问有多少种不同的方法可以爬到楼顶呢?
排列问题,在做先背包后物品时发现:
for(int j=1;j<=n;j++){
for(int i=0;i<=m;i++){
if(j-i>=0) dp[j]+=dp[j-i];
}
}
装满背包的最小数量问题
dp[j]表示背包容量为j时最小的硬币数。
注意这里的初始化:因为最大值INT_MAX,dp[0]=0,并且只有dp[j-coins[i]]!=INT_MAX时才能加上去。
递推关系式:dp[j]=min(dp[j],dp[j-coins[i]]+1)
完全背包的组合问题,与零钱兑换类似。
如果用回溯的话,aaaaaaa,aaaa这样的恶心测试用例过不了,如果加上记忆化递归可以AC,
使用memory数组保存每次计算的以startIndex起始的计算结果,如果memory[startIndex]里已经被赋值了,直接用memory[startIndex]的结果。
下面是动规部分:
单词是物品,字符串s是背包,单词能否组成s,就是问物品能不能把背包装满。(这种问题的转换很重要啊,感觉最缺少的就是这种意识)
dp[j]表示字符串长度为j时,字符串s能否被单词组成。
递推关系式:物品表示:从i=0开始,i<j过程种,如果dp[i]为true并且j-i的字符串在单词列表里,那么就代表dp[j]也为true。
多重背包问题
有N种物品,和容量为V的背包,第i种物品最多有Mi件可用,每件耗费空间是Ci,价值是Wi,求解如何装总价值最大?
其实和0-1背包问题很像,将每件物品的Mi件都摊开,就变成了0-1背包问题。
背包问题的递推关系式小结:
1、能否装满背包或最多能装多少(能否达到目标和):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
2、装满背包有多少种方法:dp[j] += dp[j - nums[i]]
3、装满背包的最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
4、装满背包的最小数量:dp[j] = min(dp[j - coins[i]] + 1, dp[j])
遍历顺序问题上:
组合 | 排列 | |
0-1背包 | 先物品后背包,背包倒序 | 先背包后物品,物品倒序 |
完全背包 | 先物品后背包,背包正序 | 先背包后物品,物品正序 |
打家劫舍问题
打家劫舍(不能连着偷)
dp[j]表示偷到下标j处的最高金额。dp[j]有两种来源方式,即偷j与不偷j,那么:
dp[j]=max(dp[j-1],dp[j-2]+nums[i])
dp数组的初始化:dp[0]=nums[0],dp[1]=max(nums[0],nums[1])。
打家劫舍II(首尾相连,不能连着偷)
也就是说要么首可以被考虑偷,要么尾可以被考虑偷。
分成两个数组一个是下标0->nums.size()-2;一个是下标1->nums.size()-1。
对这两个数组分别作打家劫舍的操作,比较取较大值。
打家劫舍III(二叉树、不能连着偷)
树形dp正式开始!!
用一个长度为2的数组作为递归函数的返回值,其中下标为0记录不偷该节点所得最大金钱,下标为1记录偷该节点所得最大金钱。
买卖股票问题
买卖股票的最佳时机(只能买卖一次)
构造二维dp数组,dp[j][0]表示到下标j时未持有股票所得最大现金;dp[j][1]表示到下标j时持有股票所得最大现金。
由于只能买卖一次,所以:
//到下标i持有,有两种情况:1.下标i-1就持有;2.i-1未持有到i时买了
dp[i][1]=max(dp[i-1][1],-prices[i]);
这里的i-1未持有到i时才买,就相当于只付了当前股票的钱,之前一直没有购入股票
买卖股票的最佳时机II(可多次买卖,但只能持有一个股票)
这里的i-1未持有到i时才买,就需要考虑之前的情况了,所以:
dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i]);
买卖股票的最佳时机III(最多完成两笔交易)
对交易次数进行限制,那么每天的状态分成了5种:没有操作(可去掉)、第一次持有股票、第一次未持有股票、第二次持有股票、第二次未持有股票。
那么最后的第二次未持有股票的状态肯定是5种状态中的最大值。
如果第一次卖出已经是最大值了,那么我们可以在当天立刻买入再立刻卖出。所以dp[4][4]已经包含了dp[4][2]的情况。也就是说第二次卖出手里所剩的钱一定是最多的。
买卖股票的最佳时机IV(最多完成k笔交易)
每天设置2k+1种状态,每种状态都由前一天的当前状态或前一状态递推而来。
含有冷冻期,一天就三个状态,未持有、持有、冷冻期。
未持有的由来:延续上一天的未持有/上一天持有今天卖了
持有的由来:延续上一天的持有/上一天是冷冻期今天又买了
冷冻期的由来:只能是两天前持有,一天前卖了(还是得和冷冻期的前一天状态比较)
另外,在初始化时,由于冷冻期的状态与两天前有关,所以初始化得初始化两天。
在卖出的时候减去手续费即可
子序列问题
dp[i]表示到下标i为止,以nums[i]结尾的最长子序列长度。
for(int i=1;i<nums.size();i++){
for(int j=0;j<i;j++){
if(nums[i]>nums[j]) dp[i]=max(dp[i],dp[j]+1);
}
}
dp[i]表示到下标i为止,以nums[i]结尾的最长连续子序列的长度。
因为连续,只需要判断当前数与前一个数即可。
dp[i][j]表示到nums1下标i-1 nums2下标j-1为止 两个数组的公共最长子数组长度
不相交的线(最长公共子序列的换皮题)
最大子序和
判断子序列
两个字符串的删除操作
dp[i][j]表示以下标i-1结尾的word1 和以下标j-1结尾的word2的最近编辑距离
注意这里的插入、删除、替换操作。插入可以看成是word2的删除
确定递推关系式:一共有4种情况
if(word1[i-1]==word2[j-1]) 不操作
if(word1[i-1]!=word2[j-1]) 插入、删除、替换