描述

给定两个字符串str1和str2,输出两个字符串的最长公共子序列。如果最长公共子序列为空,则返回"-1"。目前给出的数据,仅仅会存在一个最长的公共子序列

数据范围:[动态规划]BM65 最长公共子序列(二)-中等_动态规划要求:空间复杂度 [动态规划]BM65 最长公共子序列(二)-中等_动态规划_02 ,时间复杂度 [动态规划]BM65 最长公共子序列(二)-中等_动态规划_02

示例1

输入:

"1A2C3D4B56","B1D23A456A"

返回值:

"123456"

示例2

输入:

"abc","def"

返回值:

"-1"

示例3

输入:

"abc","abc"

返回值:

"abc"

示例4

输入:

"ab",""

返回值:

"-1"

题解

题目要求最长公共子序列,因此所有在递增的方向上字母相等的都可以纳入统计。另外,由于要返回最长子序列,因此我们不仅要请最长子序列长度,还需要记录动态规划的路径。

动态规划+递归的实现

思路如下:

动态规划问题,总是逃脱不了以下这几步:

  1. 确定dp数组所代表的意义:假设s1和s2长度分别为m和n,我们用数组dp[i][k]来表示s1前i个字符和s2的前k个字符的最长公共长度,用m[i][k]来记录每次选择的方向
  2. 确定初始条件:dp[0][i],和dp[i][0]的值都为0
  3. 确定递推关系:
  1. 如果 s1[i] == s2[k],那么dp[i][k] = dp[i-1][k-1] + 1,此时设置m[i][k]=1,表示从s1[i-1]和s2[k-1]子串递推过来
  2. 如果 s1[i] != s2[k],那么dp[i][k]等于dp[i-1][k]或者dp[i][k-1]中交大的一个,m[i][k]取2的时候表示从s1[i-1] s2[k]方向过来,取3表示从s1[i] s2[k-1]方向过来
  1. dp[m][n]就是最长子序列的长度
  2. 根据得到的路径表m确定最长子序列

注意:

  1. 参数传递的时候尽量用引用,减少不必要的计算,比如使用边界条件提前返回,不然内存、时间超限了不给过~~
  2. 实际上可以不用上面的路径记录表m,因为我们可以通过dp[i][k]与dp[i-1][k]及dp[i][k-1]的大小关系判断路径的方向

代码如下:

#include <bits/stdc++.h>

// 动态规划问题,总是逃脱不了以下这几步:
// 1. 确定dp数组所代表的意义:假设s1和s2长度分别为m和n,我们用数组arr[i][k]来表示s1前i个字符和s2的前k个字符的最长公共长度
// 2. 确定初始条件:arr[0][i],和arr[i][0]的值都为0
// 3. 确定递推关系:
// 1. 如果 s1[i] == s2[k],那么arr[i][k] = arr[i-1][k-1] + 1
// 2. 如果 s1[i] != s2[k],那么arr[i][k]等于arr[i-1][k]或者arr[i][k-1]中交大的一个
// 4. arr[m][n]就是最长子序列的长度
// 5. 但是题目要求返回最长子序列的内容,因此我们还需要另外一个表来记录我们在推导过程中的方向选择
// 1. 我们用table[i][k]来表示上一步走到当前时的方向选择
// 2. 如果s1[i] == s2[k],则表示最长子序列应该加上s1[i]
// 3. 如果s1[i] != s2[k],则最长自诩咧应该是在s1[i-1]、s2[k]或者s1[i]、s2[k-1]中产生,
// 此问题可以用递归解决
// 注意:对于arr[i][k]我们在思考问题的时候,应该认为所有小于i和k的arr的值都是已知的

std::string walk(std::string &s1, std::string &s2, int i, int k, std::vector<std::vector<int>> &m)
{
if (i == 0 || k == 0)
{
return "";
}

if (m[i][k] == 1)
{
return walk(s1, s2, i - 1, k - 1, m).append(1, s1[i - 1]);
}

if (m[i][k] == 2)
{
return walk(s1, s2, i - 1, k, m);
}
return walk(s1, s2, i, k - 1, m);
}

std::string LCS(std::string s1, std::string s2)
{
if (s1.empty() || s2.empty())
{
return "-1";
}
std::vector<std::vector<int>> dp(s1.size() + 1, std::vector<int>(s2.size() + 1, 0));
std::vector<std::vector<int>> m(s1.size() + 1, std::vector<int>(s2.size() + 1, 0));
for (int i = 1; i <= s1.size(); ++i)
{
for (int k = 1; k <= s2.size(); ++k)
{
if (s1[i - 1] == s2[k - 1])
{
dp[i][k] = dp[i - 1][k - 1] + 1;
m[i][k] = 1;
}
else
{
if (dp[i - 1][k] > dp[i][k - 1])
{
dp[i][k] = dp[i - 1][k];
m[i][k] = 2;
}
else
{
dp[i][k] = dp[i][k - 1];
m[i][k] = 3;
}
}
}
}
std::string ans{"-1"};
if (dp[s1.size()][s2.size()] != 0)
{
ans = walk(s1, s2, s1.size(), s2.size(), m);
}
return ans;
}

动态规划+栈的实现

以下代码由于牛客网官方提供

class Solution {
public:
string LCS(string s1, string s2) {
//只要有一个空字符串便不会有子序列
if(s1.length() == 0 || s2.length() == 0)
return "-1";
int len1 = s1.length();
int len2 = s2.length();
//dp[i][j]表示第一个字符串到第i位,第二个字符串到第j位为止的最长公共子序列长度
vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
//遍历两个字符串每个位置求的最长长度
for(int i = 1; i <= len1; i++){
for(int j = 1; j <= len2; 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]);
}
}
//从动态规划数组末尾开始
int i = len1, j = len2;
stack<char> s;
while(dp[i][j]){
//来自于左方向
if(dp[i][j] == dp[i - 1][j])
i--;
//来自于上方向
else if(dp[i][j] == dp[i][j - 1])
j--;
//来自于左上方向
else if(dp[i][j] > dp[i - 1][j - 1]){
i--;
j--;
//只有左上方向才是字符相等的情况,入栈,逆序使用
s.push(s1[i]);
}
}
string res = "";
//拼接子序列
while(!s.empty()){
res += s.top();
s.pop();
}
//如果两个完全不同,返回字符串为空,则要改成-1
return res != "" ? res : "-1";
}
};