动态规划-子序列和公共子串问题

515 阅读2分钟

        leetcode上关于子序列和字串问题有很多,大部分都可以用动态规划来实现。首先,先说一下这两个名词的区别,字串一定是连续的,而子序列不一定是连续的。下面对这些常见的问题做一个总结归纳。

子序列问题

300. 最长上升子序列

给定一个无序的整数数组,找到其中最长上升子序列的长度。

输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

来源:leetcode-cn.com/problems/lo…

这是一个比较经典的问题,可以用动态规划实现,也可以用二分法来实现。

动态规划

var lengthOfLIS = function(nums) {
    // dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。
    let len = nums.length;
    if(!len) return 0;
    let dp = new Array(len).fill(1);
    for (let i = 0; i < nums.length; i++) {
        for (let j = 0; j < i; j++) {
            if(nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
    }
    return Math.max(...dp);
};

516. 最长回文子序列

给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000

輸入:"bbbab"
輸出:4
一个可能的最长回文子序列为 "bbbb"

來源:leetcode-cn.com/problems/lo…

var longestPalindromeSubseq = function(s) {
    let dp = Array.from(new Array(s.length), () => new Array(s.length).fill(0));
    for (let i = 0; i < s.length; i++) {
        dp[i][i] = 1;
    }
    for (let i = s.length - 1; i >= 0; i--) {
        for (let j = i + 1; j < s.length; j++) {
            if(s[i] === s[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][s.length - 1];
};

1143. 最长公共子序列

最長公共子序列问题是非常经典的面试题目,一般采用二维DP实现。大部分比较困难的问题和这个问题都是一个套路,非常值得掌握。

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

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

例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。 

 若这两个字符串没有公共子序列,则返回 0。  

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

來源:leetcode-cn.com/problems/lo…

var longestCommonSubsequence = function(text1, text2) {
    //dp[i][j] 的含义是:对于 text1[1..i] 和 text2[1..j],它们的 LCS 长度是 dp[i][j];
    let len1 = text1.length, len2 = text2.length;
    let dp = Array.from(new Array(len1 + 1), () => new Array(len2 + 1).fill(0));
    for (let i = 1; i <= len1; i++) {
        for (let j = 1; j <= len2; j++) {
            if(text1[i - 1] === text2[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];
};

72. 编辑距离

该题是最长公共子序列问题的变种。

给你两个单词 word1word2,请你计算出将 word1 转换成 word2所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

示例

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

来源:leetcode-cn.com/problems/ed…

var minDistance = function(word1, word2) {
    let l1 = word1.length;
    let l2 = word2.length;
    if(!l1) return l2;
    if(!l2) return l1;
    let dp = [];
    dp[0] = [0];
    for (let i = 1; i <= l1; i++) {
        dp[i] = [];
        dp[i][0] = i;
    }
    for (let j = 1; j <= l2; j++) {
        dp[0][j] = j;
    }
    for (let i = 0; i < l1; i++) {
        for (let j = 0; j < l2; j++) {
            if(word1[i] === word2[j]) {
                dp[i + 1][j + 1] = dp[i][j];
                continue;
            }
            let insert = dp[i + 1][j] + 1;
            let remove = dp[i][j + 1] + 1;
            let modify = dp[i][j] + 1;
            dp[i + 1][j + 1] = Math.min(insert, remove, modify);
        }
    }
    return dp[l1][l2];
};

115. 不同的子序列

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。 

 字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)

 题目数据保证答案符合 32 位带符号整数范围。  

输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下图所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
(上箭头符号 ^ 表示选取的字母)
rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^

来源:leetcode-cn.com/problems/di…

var numDistinct = function(s, t) {
    // dp[i][j]代表t前i字符串可以由s中j字符串组成最多个数
    let n = s.length, m = t.length;
    let dp = Array.from(new Array(m + 1), () => new Array(n + 1).fill(0));
    for (let i = 0; i <= n; i++) {
        dp[0][i] = 1;
    }
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            if(t[i - 1] === s[j - 1]) {
                dp[i][j] = dp[i][j - 1] + dp[i - 1][j - 1];
            } else {
                dp[i][j] = dp[i][j - 1];
            }
        }
    }
    return dp[m][n];
};

53. 最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

来源: leetcode-cn.com/problems/ma…

贪心算法实现

var maxSubArray = function(nums) {
    if (nums == null || nums.length == 0) return 0;
    let max = Number.MIN_SAFE_INTEGER;
    for (let begin = 0; begin < nums.length; begin++) {
        let sum = 0;
        for (let end = begin; end < nums.length; end++) {
            // sum是[begin, end]的和
            sum += nums[end];
            max = Math.max(max, sum);
        }
    }
    return max;
};

分治实现

var maxSubArray = function(nums) {
    return maxSubArray(nums, 0, nums.length);
    function maxSubArray(nums, begin, end) {
        if (end - begin < 2) return nums[begin];
        let mid = (begin + end) >> 1;
        let leftMax = nums[mid - 1];
        let leftSum = leftMax;
        for (let i = mid - 2; i >= begin; i--) {
            leftSum += nums[i];
            leftMax = Math.max(leftMax, leftSum);
        }
        let rightMax = nums[mid];
        let rightSum = rightMax;
        for (let i = mid + 1; i < end; i++) {
            rightSum += nums[i];
            rightMax = Math.max(rightMax, rightSum);
        }
        return Math.max(leftMax + rightMax, 
                    Math.max(
                    maxSubArray(nums, begin, mid), 
                    maxSubArray(nums, mid, end))
                );
    }
};

动态规划实现

var maxSubArray = function(nums) {
    if (nums === null || nums.length === 0) return 0;
    let dp = nums[0];
    let max = dp;
    for (let i = 1; i < nums.length; i++) {
        if(dp <= 0) {
            dp = nums[i];
        } else {
            dp = dp + nums[i];
        }
        max = Math.max(dp, max);
    }
    return max;
};

子串问题

5. 最长回文子串

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。

来源:leetcode-cn.com/problems/lo…

var longestPalindrome = function(s) {
    let len = s.length;
    if(len < 2) return s;
    let maxL = 1, begin = 0;
    let dp = Array.from(new Array(len), () => new Array(len).fill(0));
    for (let j = 1; j < len; j++) {
        for (let i = 0; i < j; i++) {
            if(s[i] !== s[j]) {
                dp[i][j] = false;
            } else {
                if(j - i < 3) {
                    dp[i][j] = true;
                } else {
                    dp[i][j] = dp[i + 1][j - 1];
                }
            }
            if(dp[i][j] && j - i + 1 > maxL) {
                maxL = j - i + 1;
                begin = i;
            }
        }
    }
    return s.substring(begin, begin + maxL);

};

647. 回文子串

给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

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

来源:leetcode-cn.com/problems/pa…

var countSubstrings = function(s) {
    let len = s.length;
    let dp = Array.from(new Array(len), () => new Array(len).fill(0));
    let ans = 0;
    for (let i = 0; i < len; i++) {
        for (let j = 0; j <= i; j++) {
            if(s[j] === s[i]) {
                if(i - j < 2 || dp[j+1][i-1]) {
                    dp[j][i] = true;
                    ans++; 
                }
            }
        }
    }
    return ans;
};