← All notes

Leetcode记录:序列、字符串DP问题

Table of contents

子序列问题

最长递增子序列

Leetcode 300

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

dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度

int lengthOfLIS(vector<int>& nums) {
    if (nums.size() <= 1) return nums.size();
    vector<int> dp(nums.size(), 1);
    int result = 0;
    for (int i = 1; i < nums.size(); i++) {
        for (int j = 0; j < i; j++) {
            if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
        }
        if (dp[i] > result) result = dp[i]; // 取长的子序列
    }
    return result;
}

Python 实现

from bisect import bisect_left

def length_of_lis(nums):
    tails = []
    for value in nums:
        i = bisect_left(tails, value)
        if i == len(tails): tails.append(value)
        else: tails[i] = value
    return len(tails)

最长连续递增序列

Leetcode 674

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

dp[i]:以下标i为结尾的连续递增的子序列长度为dp[i]。

int findLengthOfLCIS(vector<int>& nums) {
    if (nums.size() == 0) return 0;
    int result = 1;
    vector<int> dp(nums.size() ,1);
    for (int i = 1; i < nums.size(); i++) {
        if (nums[i] > nums[i - 1]) { // 连续记录
            dp[i] = dp[i - 1] + 1;
        }
        if (dp[i] > result) result = dp[i];
    }
    return result;
}

Python 实现

# 本题仅使用 Python 内置功能,无需额外 import。
def find_length_of_lcis(nums):
    current = answer = int(bool(nums))
    for left, right in zip(nums, nums[1:]):
        current = current + 1 if right > left else 1; answer = max(answer, current)
    return answer

最长重复子数组

Leetcode 718

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

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

int findLength(vector<int>& nums1, vector<int>& nums2) {
    vector<vector<int>> dp (nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
    int result = 0;
    for (int i = 1; i <= nums1.size(); i++) {
        for (int j = 1; j <= nums2.size(); j++) {
            if (nums1[i - 1] == nums2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            }
            if (dp[i][j] > result) result = dp[i][j];
        }
    }
    return result;
}

Python 实现

# 本题仅使用 Python 内置功能,无需额外 import。
def find_length(nums1, nums2):
    dp, answer = [0] * (len(nums2) + 1), 0
    for a in nums1:
        for j in range(len(nums2) - 1, -1, -1):
            dp[j + 1] = dp[j] + 1 if a == nums2[j] else 0; answer = max(answer, dp[j + 1])
    return answer

最长公共子序列

Leetcode 1143

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

dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]

int longestCommonSubsequence(string text1, string text2) {
    vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
    for (int i = 1; i <= text1.size(); i++) {
        for (int j = 1; j <= text2.size(); j++) {
            if (text1[i - 1] == text2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }
    return dp[text1.size()][text2.size()];
}

Python 实现

# 本题仅使用 Python 内置功能,无需额外 import。
def longest_common_subsequence(a, b):
    dp = [0] * (len(b) + 1)
    for char_a in a:
        diagonal = 0
        for j, char_b in enumerate(b, 1):
            old = dp[j]; dp[j] = diagonal + 1 if char_a == char_b else max(dp[j], dp[j - 1]); diagonal = old
    return dp[-1]

不相交的线

Leetcode 1035

我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。现在,我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。以这种方法绘制线条,并返回我们可以绘制的最大连线数。

本质上就是求两个字符串的最长公共子序列的长度,代码一模一样。

最大子序和

Python 实现

# 本题仅使用 Python 内置功能,无需额外 import。
def max_uncrossed_lines(nums1, nums2):
    return longest_common_subsequence(nums1, nums2)

Leetcode 53

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

除了贪心算法之外,用DP也可以解决: dp[i]:包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]。

int maxSubArray(vector<int>& nums) {
    if (nums.size() == 0) return 0;
    vector<int> dp(nums.size());
    dp[0] = nums[0];
    int result = dp[0];
    for (int i = 1; i < nums.size(); i++) {
        dp[i] = max(dp[i - 1] + nums[i], nums[i]); // 状态转移公式
        if (dp[i] > result) result = dp[i]; // result 保存dp[i]的最大值
    }
    return result;
}

Python 实现

# 本题仅使用 Python 内置功能,无需额外 import。
def max_sub_array_dp(nums):
    dp = nums[:]
    for i in range(1, len(nums)): dp[i] = max(nums[i], dp[i - 1] + nums[i])
    return max(dp)

两字符串关系问题

判断子序列

Leetcode 392

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

双指针即可解决,也可以用dp: dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]。

注意本题如果删元素一定是字符串t,而最长公共子序列是两个字符串都可以删元素。

 bool isSubsequence(string s, string t) {
    vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));
    for (int i = 1; i <= s.size(); i++) {
        for (int j = 1; j <= t.size(); j++) {
            if (s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
            else dp[i][j] = dp[i][j - 1];
        }
    }
    if (dp[s.size()][t.size()] == s.size()) return true;
    return false;
}

Python 实现

# 本题仅使用 Python 内置功能,无需额外 import。
def is_subsequence(s, t):
    iterator = iter(t)
    return all(char in iterator for char in s)

不同的子序列

Leetcode 115

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

dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。 当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成:一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。一部分是不用s[i - 1]来匹配,个数为dp[i - 1][j]。 当s[i - 1] 与 t[j - 1]不相等时,dp[i][j]只有一部分组成,不用s[i - 1]来匹配(就是模拟在s中删除这个元素),即:dp[i - 1][j]

int numDistinct(string s, string t) {
    vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size() + 1));
    for (int i = 0; i < s.size(); i++) dp[i][0] = 1;
    // 注意是从1开始
    for (int j = 1; j < t.size(); j++) dp[0][j] = 0;
    for (int i = 1; i <= s.size(); i++) {
        for (int j = 1; j <= t.size(); j++) {
            if (s[i - 1] == t[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
            } else {
                dp[i][j] = dp[i - 1][j];
            }
        }
    }
    return dp[s.size()][t.size()];
}

Python 实现

# 本题仅使用 Python 内置功能,无需额外 import。
def num_distinct(s, t):
    dp = [1] + [0] * len(t)
    for source in s:
        for j in range(len(t) - 1, -1, -1):
            if source == t[j]: dp[j + 1] += dp[j]
    return dp[-1]

两个字符串的删除操作

Leetcode 583

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

dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。

int minDistance(string word1, string word2) {
    vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1));
    for (int i = 0; i <= word1.size(); i++) dp[i][0] = i;
    for (int j = 0; j <= word2.size(); j++) dp[0][j] = j;
    for (int i = 1; i <= word1.size(); i++) {
        for (int j = 1; j <= word2.size(); j++) {
            if (word1[i - 1] == word2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
            }
        }
    }
    return dp[word1.size()][word2.size()];
}

Python 实现

# 本题仅使用 Python 内置功能,无需额外 import。
def min_distance_delete(word1, word2):
    common = longest_common_subsequence(word1, word2)
    return len(word1) + len(word2) - 2 * common

编辑距离

Leetcode 72

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。 你可以对一个单词进行如下三种操作:插入、删除、替换一个字符。

dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。 注意插入和删除其实本质上是一样的。

 int minDistance(string word1, string word2) {
    vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1, 0));
    for (int i = 0; i <= word1.size(); i++) dp[i][0] = i;
    for (int j = 0; j <= word2.size(); j++) dp[0][j] = j;
    for (int i = 1; i <= word1.size(); i++) {
        for (int j = 1; j <= word2.size(); j++) {
            if (word1[i - 1] == word2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1];
            }
            else {
                dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
            }
        }
    }
    return dp[word1.size()][word2.size()];
}

Python 实现

# 本题仅使用 Python 内置功能,无需额外 import。
def min_distance(word1, word2):
    dp = list(range(len(word2) + 1))
    for i, first in enumerate(word1, 1):
        next_row = [i]
        for j, second in enumerate(word2, 1):
            next_row.append(dp[j - 1] if first == second else 1 + min(dp[j - 1], dp[j], next_row[-1]))
        dp = next_row
    return dp[-1]

回文问题

回文子串

Leetcode 647

给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。

布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。

当s[i]与s[j]不相等,那没啥好说的了,dp[i][j]一定是false。 当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况:

情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串。 情况二:下标i 与 j相差为1,例如aa,也是回文子串。 情况三:下标:i 与 j相差大于1的时候,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。

此时唯一要注意的就是遍历顺序,可以列优先,然后从下往上,也可以先从下往上再从左到右。 如果询问最长回文子串的长度的话只需要多加一个长度判断即可完成。

int countSubstrings(string s) {
    vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
    int result = 0;
    for (int i = s.size() - 1; i >= 0; i--) {  // 注意遍历顺序
        for (int j = i; j < s.size(); j++) {
            if (s[i] == s[j]) {
                if (j - i <= 1) { // 情况一 和 情况二
                    result++;
                    dp[i][j] = true;
                } else if (dp[i + 1][j - 1]) { // 情况三
                    result++;
                    dp[i][j] = true;
                }
            }
        }
    }
    return result;
}

Python 实现

# 本题仅使用 Python 内置功能,无需额外 import。
def count_substrings(s):
    answer = 0
    for center in range(2 * len(s) - 1):
        left, right = center // 2, (center + 1) // 2
        while left >= 0 and right < len(s) and s[left] == s[right]: answer += 1; left -= 1; right += 1
    return answer

最长回文子序列

Leetcode 516

给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。

dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]。 如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2; 如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子序列的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);

注意要初始化对角线元素:

int longestPalindromeSubseq(string s) {
    vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
    for (int i = 0; i < s.size(); i++) dp[i][i] = 1;
    for (int i = s.size() - 1; i >= 0; i--) {
        for (int j = i + 1; j < s.size(); j++) {
            if (s[i] == s[j]) {
                dp[i][j] = dp[i + 1][j - 1] + 2;
            } else {
                dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
            }
        }
    }
    return dp[0][s.size() - 1];
}

Python 实现

# 本题仅使用 Python 内置功能,无需额外 import。
def longest_palindrome_subseq(s):
    return longest_common_subsequence(s, s[::-1])

正则表达式匹配

Leetcode 10

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 .* 的正则表达式匹配。. 匹配任意单个字符,* 将前一个元素变为零个或多个

bool isMatch(string s, string p) {
        int m = s.size();
        int n = p.size();

        auto matches = [&](int i, int j){
            if(i == 0)
                return false;
            if(p[j-1] == '.')
                return true;
            return s[i-1] == p[j-1];
        };
        vector<vector<int>> dp(m+1, vector<int>(n+1, false));
        dp[0][0] = true;
        for(int i = 0; i <= m; ++i){
            for(int j = 1; j <= n; ++j){
                if(p[j-1] == '*'){
                    dp[i][j] |= dp[i][j-2];
                    if(matches(i,j-1)) //注意这一步,很巧妙!!
                        dp[i][j] |= dp[i-1][j];
                } else{
                    if(matches(i, j))
                        dp[i][j] |= dp[i-1][j-1];
                }
            }
        }
        return dp[m][n];
    }

Python 实现

# 本题仅使用 Python 内置功能,无需额外 import。
def is_match(s, p):
    dp = [[False] * (len(p) + 1) for _ in range(len(s) + 1)]; dp[0][0] = True
    for j in range(2, len(p) + 1):
        if p[j - 1] == "*": dp[0][j] = dp[0][j - 2]
    for i in range(1, len(s) + 1):
        for j in range(1, len(p) + 1):
            if p[j - 1] in {s[i - 1], "."}: dp[i][j] = dp[i - 1][j - 1]
            elif p[j - 1] == "*": dp[i][j] = dp[i][j - 2] or p[j - 2] in {s[i - 1], "."} and dp[i - 1][j]
    return dp[-1][-1]