问题类型
该类问题都是让你去找两个序列中有相同特征的公共子序列。
像这些:
- 300.最长递增子序列
- 674.最长连续递增序列
- 718.最长重复子数组
- 1143.最长公共子序列
- 1035.不相交的线
- 53.最大子序和
- 392.判断子序列
- 115.不同的子序列
- 583.两个字符串的删除操作
- 72.编辑距离
- 647.回文子串
- 516.最长回文子序列
解决方法
这里用最长递增序列来做例子(leetcode)
这是最近第一次写动态规划,已经没了思路,所以现在来学习归纳,并在这题应用下解题方法:
1. 首先第一步,试着穷举,并且画图,明确dp[i]的定义。
因为动态规划题目本质上就是空间换时间,找每一步中重复的部分,把重复的步骤用额外的空间存下来,就省去了很多重复计算的时间,然后找到相邻两步的规律,作为状态转移公式,就差不多了。而dp就是存着重复步骤的数组。
比如说这里给的例子:10,9,2,5,3,7,101,18
我们来画下图
我们可以看到,选2开头的时候,后面能有很多选择,5,3,7,101,18,其中,5,3,7后面遍历的时候会遇到,是重复的步骤,所以,可以先存下来。
存的是什么?存什么取决的是求什么。因为动态规划问题都要求一个最优解,在求解过程中都是在当前这一步找到最优解然后逐渐往后推的,我们先把上一步的最优解存起来,然后这一步面临选择的时候,比较所有情况,然后再把最优解存下来,这就是动态规划推导的步骤。
这道题的最优解是要找最长严格递增子序列长度,简单来说就是要找元素最多,最长的那条路。至于怎么求,那就要看下一步了。
2. 第二步,明确状态转移方程
状态转移方程其实就是上面说过的,就是我们先把上一步的最优解存起来,然后这一步面临选择的时候,比较上一步的最优解在这一步面临的所有情况,选择其中最优解,然后再把最优解存下来。
这么讲不直观,看看代码:
代码应该长这样:
for(所有情况) {
if(比较情况) {
满足情况
dp[这一步] = 上一步 + 最好情况 //这就是最优解
}
}
对于这一题,最好情况就是路径最长,这一步就等于上一步里面,符合递增规律的,路径最长的 + 1。我们用一个临时变量maxLen来保存最长路径。
为了再直观一点,我们还是用这道题为例子讲一遍:
在这道题中,因为处于前面的路径(10,9,2),要依靠处于后面(5,3,7,101,18)里面的路径最长并符合规律的来作为这一步的最优解,所以我们从后往前推:
- 18是路径长1,它是最尾部,
- 101也是1,因为它大于18,不符合递增规律,所以也只能作为最尾部
- 7是2,因为它可以选择到18,也可以是101,最长的就是2
- 3是3,因为它比后面的要小,可以达成(3,7,18)或者(3,101)或者(3,7,101)或者(3,18)组合,最长的有3个。
- 5是3,因为符合递增规律的只有(7,101,18),其中对应的路径长为7是2,101是1,18是1,所以最长是选择7,2+1 = 3.
- 2是4,因为符合递增规律的有(5,3,7,101,18),选择其中最长的就好了
- ...以此类推
所以,dp存的就是每一步上的最长路径。
代码:
for(从我这里开始前面所有的状态dp[i -1]) { //从2开始的话,后面就有5,3,7,101,18
let maxLen = 1
if(满足递增 && 这个状态下dp[] + 1 > maxLen) {
//替换
maxlen = dp
}
dp[i] = maxLen
}
3.初始状态
明确从一开始的初始状态。
在这道题,从后面开始往前推,一开始的18只有自己一个,就是1
let dp = new Array(nums.length)
dp[nums.length - 1] = 1
4. 遍历顺序
一般都是从左往右的啦,但也还是应该取决于你这一步取决于什么。
在这道题中,我这一步比如说在2,取决于上一步,也就是后面的5,3,7,101,18中,路径最长的一步,所以我们这题从后往前遍历
for(let i = nums.length - 2; i >= 0; i--)
5. 举例推导验证
打印出每一步,验证自己想法,完善边界条件。
完整代码
var lengthOfLIS = function(nums) {
//新建一个dp数组
let dp = new Array(nums.length)
//初始化,最后一个是1
dp[nums.length - 1] = 1
//这是等会返回的结果
let result = 1
//从倒数第二个开始遍历,因为到书第一个已经初始化了
for(let i = nums.length - 2; i >= 0; i--) {
//临时变量,存着当前最长的
let maxLen = 1
//遍历选择
for(let j = i; j < nums.length; j++) {
//判断选择是否符合条件:是否递增,路径+1后是否比当前最长路径更长
if(nums[i] < nums[j] && maxLen < dp[j] + 1) {
//符合条件,赋值
maxLen = dp[j] + 1
}
}
//保存当前最长
dp[i] = maxLen
//判断是否比全局的更长,更长的替换
if(maxLen > result) {
result = maxLen
}
}
//返回最长
return result
};
小技巧
- 一般都是dp数组存着当前最长的长度。
- 看复不复杂,不复杂尝试一维数组,复杂的尝试二维数组。
- 在二维数组里面,当前位置取值(i,j)通常会取决于(i-1,j-1)这样的位置,如果循环的时候,在i = 0或者j = 0的位置会很尴尬,因为上一步i - 1 = -1,是不存在的,所以要注意边界情况,这里解决方法通常是初始化dp的时候加多一行: