算法-动态规划-子序列

子序列问题动态规划 解决的经典问题。

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

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

思路

  • dp[i]表示以nums[i]为结尾的最长子序列的长度
  • 用j遍历0至i-1的元素,如果nums[i] > nums[j],则dp[i] = Math.max(dp[i], dp[j]+1)(可以用nums[i]追加在之前的子序列末尾)
class Solution {
    // 时间复杂度:O(n^2)
    public int lengthOfLIS(int[] nums) {
        int result = 1;
        int n = nums.length;
        // dp[i]表示 以nums[i]结尾的最长子序列长度
        int[] dp = new int[n];
        
        for(int i = 0; i<n; ++i) {
            dp[i] = 1;
        }

        for(int i = 1; i<n; ++i) {
            for(int j = 0; j<i; ++j) {
                if(nums[j] < nums[i])
                    dp[i] = Math.max(dp[i], dp[j]+1);
            }
            result = Math.max(result, dp[i]);
        }

        return result;
    }
}

2. 最长连续递增序列 (LeetCode 674)

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,
如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,
那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。

思路

  • 本题不需要使用动态规划,只需要记录当前最长连续序列的长度即可
class Solution {
    public int findLengthOfLCIS(int[] nums) {
        int result = 1;
        int temp = 1;
        for(int i = 1; i<nums.length; ++i) {
            if(nums[i] > nums[i-1])
                temp++;
            else {
                result = Math.max(result, temp);
                temp = 1;
            }
        }
        return Math.max(result, temp);
    }
}

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

给两个整数数组 nums1 和 nums2 ,返回 两个数组中公共的、长度最长的子数组的长度 。

输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。

思路

  • dp[i][j]表示以 A[i-1]B[j-1] 结尾的最长子序列长度
class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        int n1 = nums1.length;
        int n2 = nums2.length;
        int result = 0;
        
        // dp[i][j]表示以下标i-1为结尾的A,和以下标j-1为结尾的B,的最长重复子数组长度
        int[][] dp = new int[n1+1][n2+1];

        // 第一行和第一列默认初始化为0
        // 双层for循环的i和j都从1开始遍历
        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;
                    result = Math.max(result, dp[i][j]);
                }
            }
        }
        return result;
    }
}

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

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace" ,它的长度为 3 。

思路

  • 设计到两个字符串/数组的比较,很符合二维dp数组的定义,确定好递归关系进行循环遍历
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int n1 = text1.length();
        int n2 = text2.length();

        // dp[i][j]表示 A[i-1] 和 B[j-1] 的最长公共序列长度 
        int[][] dp = new int[n1+1][n2+1];

        for(int i = 1; i<=n1; ++i) {
            for(int j = 1; j<=n2; ++j) {
                if(text1.charAt(i-1) == text2.charAt(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[n1][n2];
    }
}

5. 不相交的线(LeetCode 1035)

在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。

现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足:

  • nums1[i] == nums2[j]
  • 且绘制的直线不与任何其他连线(非水平线)相交。

请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。
以这种方法绘制线条,并返回可以绘制的最大连线数。

思路

  • 本题本质上求的是两个数列的最长公共子序列,上一题的代码是一样的。
class Solution {
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
        int n1 = nums1.length;
        int n2 = nums2.length;
        // dp[i][j]表示 A[0~i-1] 和 B[0~j-1] 的最多不相交的线
        int[][] dp = new int[n1+1][n2+1];

        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;
                else 
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
            }
        }

        return dp[n1][n2];
    }
}

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

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。

class Solution {
    // 动态规划解法
    public int maxSubArray(int[] nums) {
        int n = nums.length;
        // nums[0~i]的最大子数组和
        int[] dp = new int[n];
        dp[0] = nums[0];
        int result = nums[0];

        for(int i = 1; i<n; ++i) {
            // 累加 or 从头开始
            dp[i] = Math.max(dp[i-1] + nums[i], nums[i]);            
            result = Math.max(result, dp[i]);
        }

        return result;
    }
}

7. 判断子序列 (LeetCode 392)

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。
(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

// 双指针解法
class Solution {
    public boolean isSubsequence(String s, String t) {
        int i = 0;
        int j = 0;
        while(j < t.length() && i < s.length()) {
            if(s.charAt(i) == t.charAt(j)) {
                i++;
                j++;
            } 
            else
                j++;
        }
        return (i == s.length());
    }
}

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

给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数,结果需要对 109 + 7 取模。

输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
class Solution {
    public int numDistinct(String s, String t) {
        int n1 = s.length();
        int n2 = t.length();

        // dp[i][j] 表示s[0~i-1]中t[0~j-1]出现的数量
        int[][] dp = new int[n1+1][n2+1];
        
        // 空字符串s中不含目标串t
        for(int j = 0; j<=n2; ++j)  // 第一行
            dp[0][j] = 0;
        // s需要删去所有字符得到空字符串t
        for(int i = 0; i<=n1; ++i)  // 第一列
            dp[i][0] = 1;

        for(int i = 1; i<=n1; ++i) {
            for(int j = 1; j<=n2; ++j) {
                if(s.charAt(i-1) == t.charAt(j-1))
                    // 用s[i-1]去匹配 + 不用s[i-1]去匹配
                    dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
                else 
                    dp[i][j] = dp[i-1][j];
            }
        }
        return dp[n1][n2];
    }
}

9. 两个字符串的删除操作(LeetCode 583)

给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。

输入: word1 = "sea", word2 = "eat"
输出: 2
解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"

思路

  • 可以将本题转化为最长公共子序列问题
class Solution {
    // 转化为最长公共子序列问题
    public int minDistance(String word1, String word2) {
        // 先求最长公共子序列的长度
        int n1 = word1.length();
        int n2 = word2.length();

        // dp[i][j] 表示 A[0~i-1]和B[0~j-1]的最长公共子序列的长度
        int[][] dp = new int[n1+1][n2+1];

        for(int i = 1; i<=n1; ++i) {
            for(int j = 1; j<=n2; ++j) {
                if(word1.charAt(i-1) == word2.charAt(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]);
            }
        }

        int maxCommonSubSeq = dp[n1][n2];

        // 编辑距离=n1 + n2 - 最长公共子序列长度*2
        return n1 + n2 - maxCommonSubSeq*2;
    }
}

10. 编辑距离 (LeetCode 72)

给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

思路

  • 如果字符相等,则不需要操作;否则需要进行增、删、替换。
  • 增和删除是等价的操作(因此可以只使用删除)
  • 不相等的情况下,取左上上方左方 三者中的最小值,然后加一 。
class Solution {
    public int minDistance(String word1, String word2) {
        int n1 = word1.length();
        int n2 = word2.length();

        // word1[0~i-1] 变为 word2[0~j-1] 需要的操作次数
        int[][] dp = new int[n1+1][n2+1];

        // 初始化
        for(int j = 0; j<=n2; ++j)
            dp[0][j] = j;
        for(int i = 0; i<=n1; ++i)
            dp[i][0] = i;
        

        for(int i = 1; i<=n1; ++i) {
            for(int j = 1; j<=n2; ++j) {
                // 相等时,不需要进行操作
                if(word1.charAt(i-1) == word2.charAt(j-1))
                    dp[i][j] = dp[i-1][j-1];
                else
                    // 删除word1中的元素,删除word2中的元素,替换
                    dp[i][j] = Math.min(Math.min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]) + 1;
            }
        }

        return dp[n1][n2];
    }
}

11. 回文字串(LeetCode 647)(有难度)

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。

输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

思路

  • boolean dp[i][j]表示 s.charAt(i)到s.charAt(j) 是否为回文字串
  • j-i <= 1时j-i之间不存在字串,如果 s.charAt(i) == s.charAt(j)则回文字串数量加一
  • j-i >= 2dp[i][j] = dp[i+1][j-1]
  • 注意遍历的顺序:从下往上,从左往右
class Solution {
    public int countSubstrings(String s) {
        int n = s.length();
        int result = 0;
        // dp[i][j] 表示i~j是否为回文子串(左闭右闭)
        // 只需要填充右上半部分
        boolean[][] dp = new boolean[n][n];

        // 因为有一部分需要根据 dp[i+1][j-1] 得到 dp[i][j]
        // 所以遍历顺序为 从下往上,从左往右
        for(int i = n-1; i>=0; --i) {
            for(int j = i; j<n; ++j) {
                if(s.charAt(i) == s.charAt(j)) {
                    // 单个字母 or 两个字母
                    if(j - i <= 1) {
                        dp[i][j] = true;
                        result++;
                    } 
                    // i与j之间存在字串
                    else {
                        dp[i][j] = dp[i+1][j-1];
                        if(dp[i][j] == true)    
                            result++;
                    }
                }
            }
        }

        return result;
    }
}

12. 最长回味子序列(LeetCode 516)

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

class Solution {
    public int longestPalindromeSubseq(String s) {
        int n = s.length();

        // i~j(左闭右闭)之间的最长回文子序列的长度
        int[][] dp = new int[n][n];

        for(int i = n-1; i>=0; --i) {
            for(int j = i; j<n; ++j) {
                if(s.charAt(i) == s.charAt(j)) {
                    if(j-i<=1) 
                        dp[i][j] = j-i+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][n-1];
    }
}
posted @ 2024-10-11 14:21  Frank23  阅读(17)  评论(0编辑  收藏  举报