最长公共子序列(二)
描述
给定两个字符串str1和str2,输出两个字符串的最长公共子序列。如果最长公共子序列为空,则返回"-1"。目前给出的数据,仅仅会存在一个最长的公共子序列
数据范围:0≤∣���1∣,∣���2∣≤20000≤∣str1∣,∣str2∣≤2000
要求:空间复杂度 �(�2)O(n2) ,时间复杂度 �(�2)O(n2)
示例1
输入:
"1A2C3D4B56","B1D23A456A"
返回值:
"123456"
示例2
输入:
"abc","def"
返回值:
"-1"
示例3
输入:
"abc","abc"
返回值:
"abc"
示例4
输入:
"ab",""
返回值:
"-1"
思路
与最长重复子数组确定dp数组含义的思路类似,本题dp数组的含义是:s1
的前i
个字符与s2
的前j
个字符的最长公共子序列的长度
当s1遍历到i - 1,s2遍历到j - 1时最长公共子序列的长度为
dp[i][j]
然后使用两个指针i、j分别遍历两个字符串,
如果当前遍历值相等,那么当前位置的最长公共子序列长度一定可以由上一个位置的最长公共子序列长度加1得到;
如果当前遍历值不相等,则分别让i和j回退一位,取可以使dp数组更大的那个情况作为dp数组的更新值;
每次循环后,更新当前的dp数组的最大值到res
计算完毕dp数组后,根据dp数组去遍历s1、s2来获得输出字符串out
具体实现步骤如下:
- 初始化一个空字符串
out
,用于存储最长公共子序列。- 根据动态规划的思想,从右下角开始向左上角遍历dp数组。变量
i
和j
分别表示当前遍历的位置。- 如果
s1[i - 1]
等于s2[j - 1]
,说明当前字符是公共字符,将它添加到out
的前面,并将i
和j
都向左上方移动一位。- 如果不相等,则比较
dp[i - 1][j]
和dp[i][j - 1]
的大小,选择较大的方向移动。如果二者相等,则优先向左移动。- 重复步骤3和步骤4,直到
i
或j
为0。- 返回最终得到的
out
字符串,即为最长公共子序列。
代码
#include <vector>
class Solution {
public:
string LCS(string s1, string s2) {
vector<vector<int>> dp(s1.size() + 1, vector<int>(s2.size() + 1, 0));
int res = 0;
for (int i = 1; i <= s1.size(); ++i) {
for (int j = 1; j <= s2.size(); ++j) {
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[i][j] > res) {
res = dp[i][j];
}
}
}
if (res == 0)
return "-1";
string out = "";
int i = s1.size();
int j = s2.size();
while (i > 0 && j > 0) {
if (s1[i - 1] == s2[j - 1]) {
out = s1[i - 1] + out;
i--;
j--;
} else if (dp[i - 1][j] >= dp[i][j - 1]) {
i--;
} else {
j--;
}
}
return out;
}
};
往dp
数组值较大的方向移动是为了确保选择的路径上能够包含更多的公共字符,从而得到最长的公共子序列。
举个例子来说明计算得到的dp
数组的形状:
假设s1 = "ABCBDAB"
和s2 = "BDCAB"
,则dp
数组的形状如下所示:
B D C A B
A 0 0 0 1 1
B 1 1 1 1 2
C 1 1 2 2 2
B 1 1 2 2 3
D 1 2 2 2 3
A 1 2 2 3 3
B 1 2 2 3 4
dp[i][j]
表示s1
的前i
个字符与s2
的前j
个字符的最长公共子序列的长度。根据题目要求,我们希望得到最长的公共子序列,因此,在遍历时如果当前字符相等,就选择左上方对角线上的值加1,表示当前字符也属于公共子序列;如果不相等,则选择左方或上方较大的值,表示忽略当前字符。
在这个例子中,dp[7][5]
的值为4,表示s1
的前7个字符与s2
的前5个字符的最长公共子序列的长度为4。通过回溯路径,我们可以得到最长公共子序列为"BCBA"。
在计算out
字符串时,根据上述例子,从右下角开始,按照选择的方向移动,即可得到最长公共子序列"BCBA"。