代码随想录二刷第十七和十八天 | 子序列与编辑距离专题 + 回文子串和子序列

66 阅读11分钟

300. 最长递增子序

题目:300. 最长递增子序列 - 力扣(LeetCode)

题解:代码随想录

状态:半AC

思路

  • dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度
  • if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1)
  • 初始化:i对应的dp[i]起始大小至少都是1

代码

时间复杂度:O(N^2) 空间复杂度:O(N)

class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] dp = new int[nums.length];
        Arrays.fill(dp, 1);
        int res = 1;
        for(int i = 1; i < nums.length; i++){
            for(int j = 0; j < i; j++){
                if(nums[i] > nums[j]){
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
                res = Math.max(res, dp[i]);
            }
        }
        return res;
    }
}

674. 最长连续递增序列

题目:674. 最长连续递增序列 - 力扣(LeetCode)

题解:代码随想录

状态:AC

思路

  • dp[i]:以下标i为结尾的连续递增的子序列长度为dp[i]
  • if (nums[i] > nums[i - 1]) dp[i] = dp[i - 1] + 1;
  • dp[i]应该初始为1

代码

时间复杂度:O(N) 空间复杂度:O(N)

class Solution {
    public int findLengthOfLCIS(int[] nums) {
        int[] dp = new int[nums.length];
        Arrays.fill(dp, 1);
        int res = 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;
    }
}

718. 最长重复子数组

题目:718. 最长重复子数组 - 力扣(LeetCode)

题解:代码随想录

状态:半AC,递推公式不明确

思路

特别注意:dp[i][j]是以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]

递推公式:当nums1[i - 1] == nums[j - 1]时,dp[i][j] = dp[i - 1][j - 1] + 1

代码

时间复杂度:O(N * M) 空间复杂度:O(N * M)

class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        int n1 = nums1.length, n2 = nums2.length;
        int[][] dp = new int[n1 + 1][n2 + 1];
        int res = 0;
        for (int i = 1; i <= n1; i++) {
            for (int j = 1; j <= n2; j++) {
                if (nums1[i - 1] == nums2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                    res = Math.max(res, dp[i][j]);
                }
            }
        }
        return res;
    }
}

1143.最长公共子序列

题目:1143. 最长公共子序列 - 力扣(LeetCode)

题解:代码随想录

状态:AC

思路

递推公式加一行:

if(nums1[i - 1] == nums2[j - 1]){
    dp[i][j] = dp[i - 1][j - 1] + 1;
    res = Math.max(res, dp[i][j]);
}else{
    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}

代码

时间复杂度:O(N * M) 空间复杂度:O(N * M)

class Solution {
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
        int len1 = nums1.length, len2 = nums2.length;
        int[][] dp = new int[len1 + 1][len2 + 1];
        int res = 0;
        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;
                    res = Math.max(res, dp[i][j]);
                }else{
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return res;
    }
}

1035.不相交的线

题目:1035. 不相交的线 - 力扣(LeetCode)

题解:代码随想录

状态:AC

思路

代码和上题一样,但要学会思路的转变:

  • 直线不能相交,说明在nums1中 找到一个与nums2相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,连接相同数字的直线就不会相交。

  • 本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度

代码

时间复杂度:O(N * M) 空间复杂度:O(N * M)

class Solution {
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
        int len1 = nums1.length, len2 = nums2.length;
        int[][] dp = new int[len1 + 1][len2 + 1];
        int res = 0;
        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;
                    res = Math.max(res, dp[i][j]);
                }else{
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return res;
    }
}

53. 最大子序和

题目:1035. 不相交的线 - 力扣(LeetCode)

题解:代码随想录

状态:需要多复习

思路

dp[i]:包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]

dp[i]只有两个方向可以推出来:

  • dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和
  • nums[i],即:从头开始计算当前连续子序列和

一定是取最大的,所以dp[i] = max(dp[i - 1] + nums[i], nums[i]);

代码

时间复杂度:O(N) 空间复杂度:O(N)

class Solution {
    public int maxSubArray(int[] nums) {
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        int res = nums[0];
        for(int i = 1; i < nums.length; i++){
            dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
            res = Math.max(res, dp[i]);
        }

        return res;
    }
}

392.判断子序列

题目:392. 判断子序列 - 力扣(LeetCode)

题解:代码随想录

状态:AC

思路

最长公共子序列的基础上,加个判断即可,但注意递推公式可做简化

代码

时间复杂度:O(N * M) 空间复杂度:O(N * M)

class Solution {
    public boolean isSubsequence(String s, String t) {
        int sLen = s.length(), tLen = t.length();
        if(sLen == 0) return true;
        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;
                }else{
                    dp[i][j] = dp[i][j - 1];
                }
            }
        }
        return dp[sLen][tLen] == sLen;
    }
}

115.不同的子序列

题目:115. 不同的子序列 - 力扣(LeetCode)

题解:代码随想录

状态:需要多复习

思路

  1. dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]

  2. 当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成:

  • 一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。即不需要考虑当前s子串和t子串的最后一位字母,所以只需要 dp[i-1][j-1]
  • 一部分是不用s[i - 1]来匹配,个数为dp[i - 1][j]。
  1. 当s[i - 1] 与 t[j - 1]不相等时,dp[i][j]只有一部分组成,不用s[i - 1]来匹配(就是模拟在s中删除这个元素),即:dp[i - 1][j]

  2. dp[i][0] 表示:以i-1为结尾的s可以随便删除元素,出现空字符串的个数。那么dp[i][0]一定都是1,因为也就是把以i-1为结尾的s,删除所有元素,出现空字符串的个数就是1。

代码

时间复杂度:O(N * M) 空间复杂度:O(N * M)

class Solution {
    public boolean isSubsequence(String s, String t) {
        int sLen = s.length(), tLen = t.length();
        if(sLen == 0) return true;
        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;
                }else{
                    dp[i][j] = dp[i][j - 1];
                }
            }
        }
        return dp[sLen][tLen] == sLen;
    }
}

583. 两个字符串的删除操作

题目:583. 两个字符串的删除操作 - 力扣(LeetCode)

题解:代码随想录

状态:

思路

思路1:将题目转换为求最长公共子序列的变体

思路2:将题目转换为不同子序列的变体

  1. dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数
  2. 当word1[i - 1] 与 word2[j - 1]相同的时候,dp[i][j] = dp[i - 1][j - 1];
  3. 当word1[i - 1] 与 word2[j - 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

  • 最后当然是取最小值,所以当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});

  • 因为 dp[i][j - 1] + 1 = dp[i - 1][j - 1] + 2,所以递推公式可简化为:dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);

  • 这里可能不少录友有点迷糊,从字面上理解就是当同时删word1[i - 1]和word2[j - 1],dp[i][j-1] 本来就不考虑 word2[j - 1]了,那么我在删 word1[i - 1],是不是就达到两个元素都删除的效果,即 dp[i][j-1] + 1。

  1. dp[i][0]:word2为空字符串,以i-1为结尾的字符串word1要删除多少个元素,才能和word2相同呢,很明显dp[i][0] = i。dp[0][j]的话同理

代码

时间复杂度:O(N * M) 空间复杂度:O(N * M)

(最长公共子序列变体)
class Solution {
    public int minDistance(String word1, String word2) {
        int len1 = word1.length(), len2 = word2.length();
        int res = 0;
        int[][] dp = new int[len1 + 1][len2 + 1];
        for(int i = 1; i <= len1; i++){
            for(int j = 1; j <= len2; j++){
                if(word1.charAt(i - 1) == word2.charAt(j - 1)){
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                    res = Math.max(dp[i][j], res);
                }else{
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return len1 + len2 - res * 2;
    }
}
(不同的子序列变体)
class Solution {
    public int minDistance(String word1, String word2) {
        int len1 = word1.length(), len2 = word2.length();
        int[][] dp = new int[len1 + 1][len2 + 1];
        for(int i = 0; i <= len1; i++) dp[i][0] = i;
        for(int j = 0; j <= len2; j++) dp[0][j] = j;

        for(int i = 1; i <= len1; i++){
            for(int j = 1; j <= len2; 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, dp[i][j - 1] + 1);
                }
            }
        }

        return dp[len1][len2];
    }
}

72. 编辑距离

题目:72. 编辑距离 - 力扣(LeetCode)

题解:代码随想录

状态:需要结合上面几题多复习

思路

  1. dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标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] = dp[i - 1][j] + 1、dp[i][j] = dp[i][j - 1] + 1
    换:dp[i][j] = dp[i - 1][j - 1] + 1

代码

时间复杂度:O(N * M) 空间复杂度:O(N * M)

class Solution {
    public int minDistance(String word1, String word2) {
        int len1 = word1.length(), len2 = word2.length();
        int[][] dp = new int[len1 + 1][len2 + 1];
        for(int i = 0; i <= len1; i++) dp[i][0] = i;
        for(int j = 0; j <= len2; j++) dp[0][j] = j;
        for (int i = 1; i <= len1; i++) {
            for (int j = 1; j <= len2; 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, Math.min(dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1));
                }
            }
        }
        return dp[len1][len2];
    }
}

647. 回文子串

题目:647. 回文子串 - 力扣(LeetCode)

题解:代码随想录

状态:需要多复习

思路

  1. 布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串
  2. result统计回文子串的数量
  3. 当s[i]与s[j]不相等,dp[i][j]一定是false
  4. 当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况
  • 情况一:下标i与j相同,同一个字符例如a,当然是回文子串,res++
  • 情况二:下标i与j相差为1,例如aa,也是回文子串,res++
  • 情况三:下标i与j相差大于1的时候,就取决于dp[i + 1][j - 1],为true则res++
  1. dp初始化一定全为false,因为一开始都是不匹配的
  2. 要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的

代码

时间复杂度:O(N^2) 空间复杂度:O(N^2)

class Solution {
    public int countSubstrings(String s) {
        int len = s.length();
        boolean[][] dp = new boolean[len][len];
        int res = 0;

        for (int i = len - 1; i >= 0; i--) {
            for (int j = i; j < len; j++) {
                if (s.charAt(i) == s.charAt(j)) {
                    if (j - i < 2) {
                        dp[i][j] = true;
                        res++;
                    } else if (dp[i + 1][j - 1] == true) {
                        dp[i][j] = true;
                        res++;
                    }
                }
            }
        }

        return res;
    }
}

516.最长回文子序列

题目:516. 最长回文子序列 - 力扣(LeetCode)

题解:代码随想录

状态:需要多复习

思路

  1. dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]
  2. 如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2
  3. 如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入并不能增加[i,j]区间回文子序列的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列:dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
  4. 遍历i的时候一定要从下到上遍历,这样才能保证下一行的数据是经过计算的

代码

时间复杂度:O(N^2) 空间复杂度:O(N^2)

class Solution {
    public int longestPalindromeSubseq(String s) {
        int len = s.length();
        int[][] dp = new int[len + 1][len + 1];
        for (int i = len - 1; i >= 0; i--) { // 从后往前遍历 保证情况不漏
            dp[i][i] = 1; // 初始化
            for (int j = i + 1; j < len; j++) {
                if (s.charAt(i) == s.charAt(j)) {
                    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];
    }
}