动态规划之最大子序列

61 阅读5分钟

问题类型

该类问题都是让你去找两个序列中有相同特征的公共子序列。

像这些:

  • 300.最长递增子序列
  • 674.最长连续递增序列
  • 718.最长重复子数组
  • 1143.最长公共子序列
  • 1035.不相交的线
  • 53.最大子序和
  • 392.判断子序列
  • 115.不同的子序列
  • 583.两个字符串的删除操作
  • 72.编辑距离
  • 647.回文子串
  • 516.最长回文子序列

解决方法

这里用最长递增序列来做例子(leetcode)

150bb2dc48b1d890b2c41611290cd95.png

这是最近第一次写动态规划,已经没了思路,所以现在来学习归纳,并在这题应用下解题方法:

1. 首先第一步,试着穷举,并且画图,明确dp[i]的定义。

因为动态规划题目本质上就是空间换时间,找每一步中重复的部分,把重复的步骤用额外的空间存下来,就省去了很多重复计算的时间,然后找到相邻两步的规律,作为状态转移公式,就差不多了。而dp就是存着重复步骤的数组。

比如说这里给的例子:10,9,2,5,3,7,101,18

我们来画下图

image.png

我们可以看到,选2开头的时候,后面能有很多选择,5,3,7,101,18,其中,5,3,7后面遍历的时候会遇到,是重复的步骤,所以,可以先存下来。

存的是什么?存什么取决的是求什么。因为动态规划问题都要求一个最优解,在求解过程中都是在当前这一步找到最优解然后逐渐往后推的,我们先把上一步的最优解存起来,然后这一步面临选择的时候,比较所有情况,然后再把最优解存下来,这就是动态规划推导的步骤。

这道题的最优解是要找最长严格递增子序列长度,简单来说就是要找元素最多,最长的那条路。至于怎么求,那就要看下一步了。

2. 第二步,明确状态转移方程

状态转移方程其实就是上面说过的,就是我们先把上一步的最优解存起来,然后这一步面临选择的时候,比较上一步的最优解在这一步面临的所有情况,选择其中最优解,然后再把最优解存下来。

这么讲不直观,看看代码:

代码应该长这样:

for(所有情况) {
    if(比较情况) {
        满足情况
        dp[这一步] = 上一步 + 最好情况 //这就是最优解
    }
}

对于这一题,最好情况就是路径最长,这一步就等于上一步里面,符合递增规律的,路径最长的 + 1。我们用一个临时变量maxLen来保存最长路径。

为了再直观一点,我们还是用这道题为例子讲一遍:

在这道题中,因为处于前面的路径(10,9,2),要依靠处于后面(5,3,7,101,18)里面的路径最长并符合规律的来作为这一步的最优解,所以我们从后往前推:

  1. 18是路径长1,它是最尾部,
  2. 101也是1,因为它大于18,不符合递增规律,所以也只能作为最尾部
  3. 7是2,因为它可以选择到18,也可以是101,最长的就是2
  4. 3是3,因为它比后面的要小,可以达成(3,7,18)或者(3,101)或者(3,7,101)或者(3,18)组合,最长的有3个。
  5. 5是3,因为符合递增规律的只有(7,101,18),其中对应的路径长为7是2,101是1,18是1,所以最长是选择7,2+1 = 3.
  6. 2是4,因为符合递增规律的有(5,3,7,101,18),选择其中最长的就好了
  7. ...以此类推

所以,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
};

小技巧

  1. 一般都是dp数组存着当前最长的长度。
  2. 看复不复杂,不复杂尝试一维数组,复杂的尝试二维数组。
  3. 在二维数组里面,当前位置取值(i,j)通常会取决于(i-1,j-1)这样的位置,如果循环的时候,在i = 0或者j = 0的位置会很尴尬,因为上一步i - 1 = -1,是不存在的,所以要注意边界情况,这里解决方法通常是初始化dp的时候加多一行:

c15ec5f569949652a4ce96c4208ee17.png