BM65 最长公共子序列(二)

BM65 最长公共子序列(二)

题目描述

给定两个字符串 str1str2,输出两个字符串的最长公共子序列。

如果最长公共子序列为空,则返回:

text 复制代码
"-1"

题目保证:

text 复制代码
目前给出的数据,仅存在一个最长公共子序列

数据范围

text 复制代码
0 ≤ |str1|, |str2| ≤ 2000

要求:

text 复制代码
时间复杂度:O(n²)
空间复杂度:O(n²)

示例

示例 1

输入:

text 复制代码
"1A2C3D4B56","B1D23A456A"

返回:

text 复制代码
"123456"

示例 2

输入:

text 复制代码
"abc","def"

返回:

text 复制代码
"-1"

示例 3

输入:

text 复制代码
"abc","abc"

返回:

text 复制代码
"abc"

示例 4

输入:

text 复制代码
"ab",""

返回:

text 复制代码
"-1"

解题思路

本题是经典的 最长公共子序列 LCS 问题。

注意:

text 复制代码
子序列:可以不连续,但相对顺序不能改变
子串:必须连续

例如:

text 复制代码
str1 = "abcde"
str2 = "ace"

"ace" 是公共子序列,但不是公共子串。


1. 定义 dp 数组

python 复制代码
dp[i][j]

表示:

text 复制代码
str1 前 i 个字符
和
str2 前 j 个字符
的最长公共子序列长度

例如:

text 复制代码
dp[3][2]

表示:

text 复制代码
str1 前 3 个字符
和
str2 前 2 个字符
的最长公共子序列长度

2. 为什么 dp 要多一行一列?

创建:

python 复制代码
dp = [[0] * (m + 1) for _ in range(n + 1)]

原因是要表示空字符串:

text 复制代码
dp[0][j]:str1 是空串
dp[i][0]:str2 是空串

空串和任何字符串的最长公共子序列长度都是 0


3. 状态转移

情况 1:当前字符相等

python 复制代码
if str1[i - 1] == str2[j - 1]:

说明这个字符可以加入公共子序列。

所以:

python 复制代码
dp[i][j] = dp[i - 1][j - 1] + 1

原因:

text 复制代码
当前两个字符已经匹配成功
所以两个字符串都要往前退一位

情况 2:当前字符不相等

python 复制代码
else:

说明这两个字符不能同时作为公共子序列的一部分。

只能选择:

text 复制代码
不要 str1 当前字符
或者
不要 str2 当前字符

所以:

python 复制代码
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

4. 为什么还要倒推?

前面的 dp 表只保存了:

text 复制代码
最长公共子序列的长度

但题目要求返回:

text 复制代码
具体的最长公共子序列字符串

所以需要从 dp[n][m] 开始倒推。

倒推规则:

text 复制代码
如果 str1[i - 1] == str2[j - 1]
说明这个字符属于答案,加入 res,然后 i--、j--

如果不相等
看 dp[i - 1][j] 和 dp[i][j - 1] 谁更大
往更大的方向走

因为是从后往前找,所以最后需要反转:

python 复制代码
res[::-1]

Python 代码

python 复制代码
class Solution:
    def LCS(self, s1: str, s2: str) -> str:
        n: int = len(s1)
        m: int = len(s2)

        # dp[i][j] 表示:
        # s1 前 i 个字符 和 s2 前 j 个字符 的最长公共子序列长度
        dp = [[0] * (m + 1) for _ in range(n + 1)]

        # 1. 填 dp 表
        for i in range(1, n + 1):
            for j in range(1, m + 1):
                if s1[i - 1] == s2[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1] + 1
                else:
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

        # 没有公共子序列
        if dp[n][m] == 0:
            return "-1"

        # 2. 从右下角倒推出具体字符串
        res = []
        i = n
        j = m

        while i > 0 and j > 0:
            if s1[i - 1] == s2[j - 1]:
                res.append(s1[i - 1])
                i -= 1
                j -= 1
            else:
                if dp[i - 1][j] >= dp[i][j - 1]:
                    i -= 1
                else:
                    j -= 1

        # 因为倒推得到的是倒序,所以要反转
        return ''.join(res[::-1])

复杂度分析

设:

text 复制代码
n = len(s1)
m = len(s2)

时间复杂度

text 复制代码
O(n × m)

原因:

text 复制代码
需要遍历整个 dp 二维表

空间复杂度

text 复制代码
O(n × m)

原因:

text 复制代码
需要保存 dp 二维数组

核心

python 复制代码
if s1[i - 1] == s2[j - 1]:
    dp[i][j] = dp[i - 1][j - 1] + 1
else:
    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

最终:

text 复制代码
前面双重 for:求最长公共子序列长度
后面 while:倒推出最长公共子序列字符串