DP总结
一.状态表示
dp中首要的就是定义一个dp表,一般根据具体题目再来具体分析,接下来列出一些常见的类型:
- dp[i] : 以i位置结束或者开始,……
- dp[i][j] : 以i位置以及j位置的元素为结尾,……
- dp[i] : 以[0,i] 区间上,……
- 定义多个dp表,来表示不同情况,比如dp[i][1/2/3]表示三种不同情况
- dp[i][j] : a 字符串 [0, j] 区间内的xx(⼦串),能否匹配字符串 b 的 [0, i] 区间内的xx(字串), ……(两数组的dp问题中常用)
- dp[i][j] : 从前 i 个物品中挑选,总体积不超过 j ,所有的选法中,能挑选出来的最⼤价值。(背包问题,有两个条件)//选or不选
- dp[i][j][k] 表⽰:从前 i 个字符串中挑选,字符 0 的个数不超过 j ,字符 1 的个数不超过 k ,所有的选法中,最⼤的⻓度(适用于三个要求的,例如完全背包)
二.状态转移方程
不要背状态转移⽅程,因为题型变化之后,状态转移⽅程就会跟着变化。我们要记住的是分析问题的模式。⽤这种分析问题的模式来解决问题
但很多dp题,都可以从最后一步,最后一个来入手
- 线性 dp 状态转移⽅程分析⽅式,⼀般都是根据「最后⼀步」的状况,来分情况讨论
- 根据「最后⼀个位置」的元素,结合题⽬的要求,分情况讨论
三.初始化
根据「状态转移⽅程」来决定
tips:
-
在最前⾯加上⼀个辅助结点,帮助我们初始化。使⽤这种技巧要注意两个点:
i. 辅助结点⾥⾯的值要保证后续填表是正确的;
ii. 下标的映射关系。
-
在处理字符串时,要注意
i. 「空串」是有研究意义的,因此我们将原始 dp 表的规模多加上⼀⾏和⼀列,表⽰空串。
ii. 引⼊空串后,⼤⼤的⽅便我们的初始化。
iii. 但也要注意「下标的映射关系」,以及⾥⾯的值要「保证后续填表是正确的」。
iiii.如果dp多开了一行一列,字符串也可以多加一个空,使字符串下标与dp相对应 eg: s = ‘ ’ + s;
四.填表顺序
根据「状态转移方程」来决定,从上往下,从左往右,先上再右等各类情况
五.返回值
根据「状态表⽰」来决定,据题意分析,一般较易
注意事项:有的题目在问返回值时,要注意你的dp表定义时,里面有没有重复的,如果有记得去重
六.技巧
动态规划没有固定的模板,但有一些思想和式子也有助于我们做题:
-
正难则反
-
当我们发现,计算⼀个状态的时候,需要⼀个循环才能搞定的时候,我们要想到去优化。优化的⽅向就是⽤⼀个或者两个状态来表⽰这⼀堆的状态。通常就是把它写下来,然后⽤数学的⽅式做⼀下等价替换,推导过程如下
eg : 状态转移⽅程为:dp[i][j] = dp[i][j - 1] || dp[i - 1][j - 1] || dp[i - 2][j - 1] …
我们发现 i 是有规律的减⼩的,因此我们去看看 dp[i - 1][j] :dp[i - 1][j] = dp[i - 1][j - 1] || dp[i - 2][j - 1] || dp[i - 3][j - 1] … 我们惊奇的发现, dp[i][j] 的状态转移⽅程⾥⾯除了第⼀项以外,其余的都可以⽤ dp[i - 1][j] 替代。
因此,我们优化我们的状态转移⽅程为: dp[i][j] = dp[i][j - 1] || dp[i - 1][j]实际上,就是将i换成了i-1,因为这题j是固定所以换i,如果有题目j不固定,i是固定的话,也可以换j
eg2:dp[i][j] = min(dp[i - 1][j], dp[i][j - coins[i]] + 1) 。
就是相当于把第⼆种情况 dp[i - 1][j - coins[i]] + 1 ⾥⾯的 i - 1 变成 i 即可。
-
有的题目需要反复检查数据时,可以考虑⽤哈希表做优化,甚至直接在哈希表中做动态规划。
-
背包问题解决的是组合,不是排列问题。排列问题要求有序 ,组合是无序的。
-
空间优化:
背包问题基本上都是利⽤「滚动数组」来做空间上的优化:
i. 利⽤「滚动数组」优化;
ii. 直接在「原始代码」上修改。
在01背包中,优化一般都是
i. 删掉第⼀维;
ii. 修改第二层循环的遍历顺序即可。
在完全背包(可以拿无限多个)里则是:
i. 仅需删掉所有的横坐标
对于⼆维费⽤的 01 背包类型的,我们的优化策略是:
i. 删掉第⼀维;
ii. 修改第⼆层以及第三层循环的遍历顺序即可
-
我们一般为了方便会给dp表多开一行多开一列,但在有的题目里,例如求最大最小值的,如果不初始化好的话,可能会影响到后面的结果,所以推荐用0x3f3f3f3f来初始化,既能表示出最大最小的效果,又能进行±运算。