动态规划(四)子序列、编辑距离

子序列问题

子序列可能拥有的性质:

  1. 连续:即子数组(数组场景)、子串(字符串场景)
  2. 递增

解题总结:

  • 子序列可以是不连续的;
  • 子数组(子字符串)需要是连续的;
  • 当单个数组或者字符串要用动态规划时,可以把动态规划dp[i]定义为nums[0:i] 中想要求的结果;
  • 当两个数组或者字符串要用动态规划时,可以把动态规划定义成两维的 dp[i][j] ,其含义是在A[0:i−1] 与B[0:j−1] 之间匹配得到的想要的结果。
  • DP数组定义为以nums[i]为结尾,需要遍历dp数组来得到结果;定义为范围[0,i-1]内,则dp的最后一个元素即为结果
  • DP数组定义为 数量,最后一位相等时需要考虑不使用最后一位的情况,定义为长度则直接+1

1、单个序列,最长递增类

300. 最长递增子序列(递增子序列)

  1. 子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序
  2. 解题步骤:
  • dp数组含义:以下标i为结尾的最长递增子序列的长度

要求序列有序,所以必须确定序列最后一个元素的值,才能比较新加入序列的元素是不是递增的

  • 递推公式:for(j<i, nums[i] > nums[j]):dp[i] = max(dp[j]+1, dp[i]) 。位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值,取dp[j] + 1的最大值
  • 初始化:dp[i] = 1
  • 遍历顺序:dp[i] 是由0到i-1各个位置的最长递增子序列 推导而来。用i从小到大遍历子序列,代表以nums[i]为结尾的子序列。再用j遍历0到i-1,计算dp[i]。
  1. 结果:在以下标i为结尾的最长递增子序列的长度中取最大值
class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] dp = new int[nums.length];
        int res = 0;
        for(int i = 0; i < nums.length; i++){
            dp[i] = 1;
            for(int j = 0; j < i; j++){
                if(nums[i] > nums[j])   dp[i] = Math.max(dp[i],dp[j]+1);
            }
            res = Math.max(dp[i],res);
        }
        return res;
    }
}

674. 最长连续递增序列(递增子数组)

  1. 解题步骤:
  • dp数组含义:以下标i为结尾的连续递增的子序列长度为dp[i]。一定是以下标i为结尾,并不是说一定以下标0为起始位置
  • 递推公式:if( nums[i] > nums[i-1]):dp[i] = dp[i-1] + 1 。本题要求连续递增子序列,所以就只要比较nums[i]与nums[i - 1],而不用去比较nums[j]与nums[i] (j是在0到i之间遍历)
  • 初始化:dp[i] = 1
  1. 结果:在所有以下标i为结尾的最长连续递增子序列的长度中取最大值
动态规划
class Solution {
    public int findLengthOfLCIS(int[] nums) {
        if(nums.length == 1)    return 1;
        int res = 0;
        int[] dp = new int[nums.length];
        for(int i = 0; i < nums.length; i++){
            dp[i] = 1;
        }
        for(int i = 1; i < nums.length; i++){
            if(nums[i] > nums[i-1]) dp[i] = dp[i-1] + 1;
            res = Math.max(res,dp[i]);
        }
        return res;
    }
}
贪心
class Solution {
    public int findLengthOfLCIS(int[] nums) {
        if(nums.length == 1)    return 1;
        int res = 0;
        int max = 1;
        for(int i = 1; i < nums.length; i++){
            if(nums[i] > nums[i-1]) max++;
            else{
                max = 1;
            }
            res = Math.max(max,res);
        }
        return res;
    }
}

2、两个序列

718. 最长重复子数组(子数组)

  1. 子数组其实是连续子序列
  2. 解题步骤:
  • dp数组含义:以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。“以下标i - 1为结尾的A” 标明一定是 以A[i-1]为结尾的子数组

连续相等子序列,则必须确定序列最后一个元素的值

  • 递推公式:
    • 当nums1[i - 1] 和nums2[j - 1]相等时, dp[i][j]的状态由dp[i - 1][j - 1]推导出来,故 dp[i][j] = dp[i-1][j-1] + 1
    • 当nums1[i - 1] 和nums2[j - 1]不相等时,由于子数组的连续性,前缀数组不能为它们俩提供公共长度,故dp[i][j] = 0
  • 初始化:长度为0时,dp[0][j] = 0 ,dp[i][0] = 0
  • 遍历顺序:从小到大,先遍历nums1或者nums2都可以,遍历时记录最大值
class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        int[][] dp = new int[nums1.length+1][nums2.length+1];
        int res = 0;
        for(int i = 1; i <= nums1.length; i++){
            for(int j = 1; j <= nums2.length; j++){
                if(nums1[i-1] == nums2[j-1])    dp[i][j] = dp[i-1][j-1] + 1;
                //表明了一定是以下标i为结尾
                else dp[i][j] = 0; 
                res = Math.max(dp[i][j], res);
            }
        }
        return res;
    }
}
  1. 结果:题目要求长度最长的子数组的长度。所以在遍历的时候顺便把dp[i][j]的最大值记录下来。

1143. 最长公共子序列(子序列)

  1. 元素可以不连续;
  2. 解题步骤:
  • dp数组含义:[0, i - 1]范围内的字符串text1与[0, j - 1]范围内的字符串text2的最长公共子序列长度为dp[i][j]

不必连续的相等子序列,就不需要知道序列最后一个元素的值,只要知道范围内相等的序列长度就行,新来的相等元素可以直接加在序列后面

  • 递推公式:
    • if( text1[i-1] == text2[j-1]), dp[i][j] = dp[i - 1][j - 1] + 1。必然使用text1[i-1] 与 text2[j-1] 时
    • if(text1[i-1] != text2[j-1]), dp[i][j] = max(dp[i-1][j],dp[i][j-1])。必然不使用text1[i-1] 与 必然不使用 text2[j-1] 时
  • 遍历顺序
  • 举例推导: text1[i-1] 不等于 text2[j-1]的情况下,比如对于 ace 和 bc 而言,他们的最长公共子序列的长度等于 ① ace 和 b 的最长公共子序列长度0 与 ② ac 和 bc 的最长公共子序列长度1 的最大值,即 1。
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int len1 = text1.length(), len2 = text2.length();
        int[][] dp = new int[len1 + 1][len2 + 2];
        int res = 0;
        for(int i = 1; i <= len1; i++){
            for(int j = 1; j <= len2; j++){
                if(text1.charAt(i-1) == text2.charAt(j-1)) dp[i][j] = dp[i-1][j-1] + 1;
                //这里就说明了不一定以text1[i-1]和text2[j-1]为结尾
                else    dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
            }
        }
        return dp[len1][len2];
    }
}
  1. 结果:按照dp数组的定义,dp[text1.size()][text2.size()]就为最终结果

1035. 不相交的线(子序列)

实际上求的就是最长公共子序列

class Solution {
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
        int len1 = nums1.length;
        int len2 = nums2.length;
        int[][] dp = new int[len1+1][len2+1];
        for(int i = 1; i <= len1; i++){
            for(int j = 1; j <= len2; j++){
                if(nums1[i-1] ==  nums2[j-1])   dp[i][j] = dp[i-1][j-1] + 1;
                else    dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
            }
        }
        return dp[len1][len2];
    }
}

53. 最大子数组和(子数组)

动态规划
  1. 解题步骤:
  • dp含义:以nums[i]为结尾的最大连续子序列和为dp[i]
  • 递推公式:dp[i] = Math.max(nums[i],dp[i-1]+nums[i])
  • 初始化:dp[0] = nums[0], res = dp[0]
  1. 结果:要找最大的连续子序列,就应该找每一个i为终点的连续最大子序列。遍历时维护一个最大的dp[i]。
class Solution {
    public int maxSubArray(int[] nums) {
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        int res = dp[0];
        for(int i = 1; i < nums.length; i++){
            dp[i] = Math.max(nums[i],dp[i-1]+nums[i]);
            res = Math.max(dp[i],res);
        }
        return res;
    }
}
贪心解法

思路:计算累加和,计算后每次都要比较更新一遍最大值,如果累计和小于0则重置累加和为0,代表从下个元素开始重新计算。
思路简单,但是代码比较难写,错误较多

class Solution {
    public int maxSubArray(int[] nums) {
        int sum = 0;
        int res = -10010;
        for(int i = 0; i < nums.length; i++){
            sum+=nums[i];
            res = Math.max(res,sum);
            if(sum < 0) sum = 0;
        }
        return res;
    }
}

3、编辑距离类

392. 判断子序列(子序列)

动态规划
  1. 编辑距离的入门题目,因为从题意中我们也可以发现,只需要计算删除的情况,不用考虑增加和替换的情况
  2. 问题等价转换在 t 中找到和 s 相同子序列的长度 == s 的大小
  3. 解题步骤:
  • dp数组含义:[0, i - 1]范围内的字符串s与[0, j - 1]范围内的字符串t的相同子序列长度为dp[i][j]
  • 递推公式:
    • if s[i−1]==t[j−1], dp[i][j]=dp[i−1][j−1]+1。必然使用s[i-1] 与 t[j-1] 时
    • if s[i−1]!=t[j−1], dp[i][j]=dp[i][j−1]。必然不使用 t[j-1] 时
  • 初始化
  • 遍历顺序
class Solution {
    public boolean isSubsequence(String s, String t) {
        int slen = s.length();
        int tlen = t.length();
        int[][] dp = new int[slen+1][tlen+1];
        for(int i = 1; i <= slen; i++){
            for(int j = 1; j <= tlen; j++){
                if(s.charAt(i-1) == t.charAt(j-1))  dp[i][j] = dp[i-1][j-1] + 1;
                //表明了不一定要以t[j-1]为结尾,但是一定要以s[i-1]为结尾
                else    dp[i][j] = dp[i][j-1];
            }
        }
        return dp[slen][tlen] == slen;
    }
}
暴力
class Solution {
    public boolean isSubsequence(String s, String t) {
        if(t.length() == 0 && s.length() != 0)   return false;
        if(s.length() == 0)   return true;
        int slen = s.length();
        int tlen = t.length();
        int c = 0;
        for(int i = 0; i < tlen; i++){
            if(s.charAt(c) == t.charAt(i)){
                c++;
                if(c == slen)   return true;
            }
        }
        return false;
    }
}

115. 不同的子序列(子序列)

  1. 解题步骤:
  • dp数组含义:[0, i - 1]范围内的字符串s,其子序列中出现j-1为结尾的字符串t的个数为dp[i][j]
  • 递推公式:
    • if s[i-1] == t[j-1]dp[i][j] = dp[i-1][j-1] + dp[i-1][j]。使用s[i-1]的情况+考虑不使用s[i-1]的情况;
    • if s[i-1] != t[j-1]dp[i][j] = dp[i-1][j]。只考虑不使用s[i-1]的情况。

为什么还要考虑 不用s[i - 1]来匹配。例如: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,但是字符串s也可以不用s[3]来匹配,即用s[0]s[1]s[2]组成的bag。

  • 初始化:d[i][0] = 1,dp[0][j] = 0,dp[0][0] = 1;
  • 遍历顺序
  • 举例推导
class Solution {
    public int numDistinct(String s, String t) {
        int slen = s.length();
        int tlen = t.length();
        int[][] dp = new int[slen+1][tlen+1];
        for(int i = 0; i <= slen; i++) dp[i][0] = 1;
        for(int j = 1; j <= tlen; j++) dp[0][j] = 0;
        for(int i = 1; i <= slen; i++){
            for(int j = 1; j <= tlen; j++){
                if(s.charAt(i-1) == t.charAt(j-1))  dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
                //表明了不一定以s[i-1]为结尾,但是一定以t[j-1]为结尾
                else    dp[i][j] = dp[i-1][j];
            }
        }
        return dp[slen][tlen];
    }
}

583. 两个字符串的删除操作(子序列)

  1. 两个字符串可以相互删
  2. 解题步骤:
  • dp数组:[0, i - 1]范围内的字符串word1,和[0, j - 1]范围内的字符串word2,形成相同字符串的最小删除次数
  • 递推公式:
    • if word1[i - 1] == word2[j - 1], dp[i][j] = dp[i-1][j-1]
    • if word1[i - 1] != word2[j - 1], dp[i][j] = min(dp[i-1][j-1]+2,dp[i-1][j]+1,dp[i][j-1]+1) 。删word1[i - 1],最少操作次数为dp[i - 1][j] + 1;删word2[j - 1],最少操作次数为dp[i][j - 1] + 1;同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2。
  • 初始化:dp[i][0] = i,dp[0][j] = j
  • 遍历顺序
  • 举例推导
class Solution {
    public int minDistance(String word1, String word2) {
        int w1len = word1.length();
        int w2len = word2.length();
        int[][] dp = new int[w1len+1][w2len+1];
        for(int i = 0; i <= w1len; i++)  dp[i][0] = i;
        for(int j = 0; j <= w2len; j++)  dp[0][j] = j;
        for(int i = 1; i <= w1len; i++){
            for(int j = 1; j <= w2len; j++){
                if(word1.charAt(i-1) == word2.charAt(j-1)){
                    dp[i][j] = dp[i-1][j-1];
                }else{
                    dp[i][j] = Math.min(dp[i-1][j-1]+2,Math.min(dp[i-1][j],dp[i][j-1])+1);
                }
            }
        }
        return dp[w1len][w2len];
    }
}

72. 编辑距离(子序列)

  1. 可以删除、添加、替换
  2. 解题步骤:
  • dp数组:[0, i - 1]范围内的字符串word1,和[0, j - 1]范围内的字符串word2,最近编辑距离为dp[i][j]
  • 递推公式:
    • if (word1[i - 1] == word2[j - 1]), dp[i][j] = dp[i - 1][j - 1]
    • if (word1[i - 1] != word2[j - 1]), dp[i][j] = min(dp[i][j] = dp[i - 1][j] , dp[i][j] = dp[i][j - 1] , dp[i][j] = dp[i - 1][j - 1] ) + 1。操作一:word1删除一个元素;操作二:word2删除一个元素;操作三:替换元素。word2添加一个元素,相当于word1删除一个元素
  • 初始化:dp[i][0] = i,dp[0][j] = j
  • 遍历顺序
  • 举例推导
class Solution {
    public int minDistance(String word1, String word2) {
        int w1len = word1.length();
        int w2len = word2.length();
        int[][] dp = new int[w1len+1][w2len+1];
        //dp[i][j]:[0,i-1],[0,j-1]
        for(int i = 0; i <= w1len; i++) dp[i][0] = i;
        for(int j = 0; j <= w2len; j++) dp[0][j] = j;
        for(int i = 1; i <= w1len; i++){
            for(int j = 1; j <= w2len; j++){
                if(word1.charAt(i-1) == word2.charAt(j-1)){
                    dp[i][j] = dp[i-1][j-1];
                }else{
                    dp[i][j] = Math.min(Math.min( dp[i - 1][j], dp[i][j - 1]),dp[i - 1][j - 1]) + 1;
                }
            }
        }
        return dp[w1len][w2len];
    }
}

4、回文类

647. 回文子串(子串)

  1. 解题思路:判断一个子字符串[i,j]是否回文,依赖于,子字符串[i + 1, j - 1]是否是回文。若子字符串[i,j]回文,则res++。用二维的DP数组来表示一段范围内的字符串是否为回文字符串
  2. 解题步骤:
  • dp数组含义:dp[i][j]表示[i, j]范围内的字串是否为回文子串
  • 递推公式:
    • if( dp[i] != dp[j]), d[i][j] = false
    • if (dp[i] == dp[j])。(1)if( i == j || j - i == 1),dp[i][j] = true,比如a,aa。(2)if( j - i > 1 ), dp[i][j] = dp[i+1][j-1],看子字符串[i + 1, j - 1]是否是回文
  • 初始化:dp[i][j] = false
  • 遍历顺序: dp[i][j]依赖于dp[i+1][j-1],遍历i时从大往小,遍历j时从小往大
    遍历顺序
class Solution {
    public int countSubstrings(String s) {
        int len = s.length();
        int res = 0;
        char[] str = s.toCharArray();
        boolean[][] dp = new boolean[len][len];
        for(int i = len - 1; i >= 0; i--){
            for(int j = i; j < len; j++){
                if(str[i] == str[j]){
                    if(j - i <= 1)  dp[i][j] = true;
                    else    dp[i][j] = dp[i+1][j-1];
                }
                if(dp[i][j])    res++;
            }
        }
        return res;
    }
}

516. 最长回文子序列(子序列)

  1. 解题步骤:
  • dp数组含义:字符串s在[i, j]范围内最长回文子序列的长度为dp[i][j]
  • 递推公式:
    • if( dp[i] != dp[j]), dp[i][j] = max(dp[i+1][j],dp[i][j-1])。分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列;
    • if (dp[i] == dp[j])。(1)if( i == j ),dp[i][j] = 1,比如a。(2)if( j > i ), dp[i][j] = dp[i+1][j-1]+2
  • 初始化:dp[i][i] = 1,这里的初始化过程搬到了递推公式时做
  • 遍历顺序:遍历i时从大往小,遍历j时从小往大
class Solution {
    public int longestPalindromeSubseq(String s) {
        char[] str = s.toCharArray();
        int len = s.length();
        int[][] dp = new int[len][len];
        for(int i = len - 1; i >= 0; i--){
            for(int j = i; j < len; j++){
                if(str[i] == str[j]){
                    if(j == i)  dp[i][j] = 1;
                    else    dp[i][j] = dp[i+1][j-1]+2;
                }else{
                    dp[i][j] = Math.max(dp[i+1][j],dp[i][j-1]);
                }
            }
        }
        return dp[0][len-1];
    }
}

混淆点

1. 为什么两个子序列最后一位相等时,有时需要考虑不使用最后一位的情况?

看dp数组的含义,题目《1143. 最长公共子序列》中DP数组定义的是长度,最后一位相等时,确实是长度+1,而题目《115. 不同的子序列》中DP数组定义的是数量,最后一位相等时,长度确实时+1,但是数量=使用最后一位的数量+不使用最后一位的数量。
因此题目《392. 判断子序列》可以将DP定义为长度或者是数量,解法都没有问题

2. DP数组定义为:(1)以nums[i-1]为结尾;(2)[0, i - 1]范围内,不一定以nums[i-1]为结尾

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
编辑距离算法是一种常用的字符串相似度度量方法,它衡量两个字符串之间的差异程度。动态规划是解决编辑距离问题的常用方法之一。 编辑距离算法的目标是通过一系列的编辑操作(插入、删除、替换)将一个字符串转换成另一个字符串,使得转换的代价最小。代价可以通过插入、删除和替换操作的权重来定义,通常情况下插入和删除的代价为1,替换的代价为2。 动态规划算法解决编辑距离问题的思路是将原始问题分解为多个子问题,并利用子问题的最优解来求解原始问题的最优解。具体步骤如下: 1. 定义状态:假设两个字符串分别为s1和s2,定义dp[i][j]为将s1的前i个字符转换成s2的前j个字符所需要的最小编辑距离。 2. 初始化边界条件:将空字符串转换成任意字符串所需要的编辑距离为对应字符串的长度,即dp[0][j] = j,dp[i][0] = i。 3. 状态转移方程:根据题目要求和定义的状态,推导出状态转移方程。对于任意位置(i, j),有以下三种情况: - 若s1的第i个字符等于s2的第j个字符,则不需要进行编辑操作,编辑距离与dp[i-1][j-1]相同,即dp[i][j] = dp[i-1][j-1]。 - 若s1的第i个字符不等于s2的第j个字符,则可以进行插入、删除或替换操作,选择代价最小的操作。具体操作可分别表示为: - 插入操作:将s1的前i个字符转换成s2的前j-1个字符,再插入s2的第j个字符,此时编辑距离为dp[i][j-1] + 1。 - 删除操作:将s1的前i-1个字符转换成s2的前j个字符,再删除s1的第i个字符,此时编辑距离为dp[i-1][j] + 1。 - 替换操作:将s1的前i-1个字符转换成s2的前j-1个字符,再将s1的第i个字符替换成s2的第j个字符,此时编辑距离为dp[i-1][j-1] + 2(若替换前后两个字符相同,则代价为0)。 综上所述,状态转移方程为:dp[i][j] = min(dp[i-1][j-1] + cost, dp[i][j-1] + 1, dp[i-1][j] + 1),其中cost表示s1的第i个字符和s2的第j个字符是否相等。 4. 求解最优解:根据状态转移方程,利用动态规划自底向上地填充dp数组。最终,dp[m][n]即为将s1转换成s2所需要的最小编辑距离,其中m和n分别为s1和s2的长度。 通过动态规划求解编辑距离算法,可以高效地计算字符串间的相似度,并在自然语言处理、拼写检查、基因序列比对等领域得到广泛应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值