https://leetcode.cn/problems/shortest-common-supersequence/description/
给你两个字符串 str1 和 str2,返回同时以 str1 和 str2 作为 子序列 的最短字符串。如果答案不止一个,则可以返回满足条件的 任意一个 答案。
如果从字符串 t 中删除一些字符(也可能不删除),可以得到字符串 s ,那么 s 就是 t 的一个子序列。
示例 1:
输入:str1 = "abac", str2 = "cab"
输出:"cabac"
解释:
str1 = "abac" 是 "cabac" 的一个子串,因为我们可以删去 "cabac" 的第一个 "c"得到 "abac"。
str2 = "cab" 是 "cabac" 的一个子串,因为我们可以删去 "cabac" 末尾的 "ac" 得到 "cab"。
最终我们给出的答案是满足上述属性的最短字符串。
示例 2:
输入:str1 = "aaaaaaaa", str2 = "aaaaaaaa"
输出:"aaaaaaaa"
提示:
1 <= str1.length, str2.length <= 1000
str1 和 str2 都由小写英文字母组成。
解答
本题使用dp解答。 有两种方案:
1 公共子序列
2 编辑距离
先看第一种方案 公共子序列
dp[i][j] 表示能包含str1[1~i]和str2[1~j]的字符串作为子序列的答案字符串最短长度。
那么起始值 dp[i][0]=i; dp[0][i] =i; 也就是包含空串和一个strx字符串作为子序列的答案字符串最短长度自然就是那个strx字符串的长度
另一个难点 从dp中推导正确的字符串。
由上图可知 dp[i][j] 肯定是从dp[i-1][j-1] dp[i-1][j] dp[i][j-1]转化而来
也代表选择某个字母 那么我们通过比较即可知道应该选择哪个字符串的哪个字母。
我们从后到前推导
当str1[i] ==str2[j]时,都是同一个字母,那么毫无悬念,选择该字母加入答案字符串. 两个字符串的索引向前移动一位
当str1[i] !=str2[j]时,根据dp的比较 确定选择哪个字符串的哪个字母, 然后做出改动的字符串的索引向前移动一位
由于我们从后到前推到 所以得到的公共部分需要翻转.
当str1 或者str2的字母全部进入答案字符串,也就是一个字符串此时为空另一个字符串有剩余的字母没处理,
那么把剩余的字符串全部加入到答案字符串的前面即可(因为我们是从后到前推导的)
那么得到正确dp答案的代码如下
class Solution {
public:
vector<vector<int>> dp;
string shortestCommonSupersequence(string s1, string s2) {
dp.resize(1010, vector<int>(1010, 0x3f3f3f3f)); //初始化dp
//字符串插入字符 dp从1索引开始 避免考虑边界问题
s1.insert(s1.begin(), '#'); s2.insert(s2.begin(),'@');
//初始化dp起始值
//dp[i][j] 表示能包含str1[1~i]和str2[1~j]的字符串作为子序列的答案字符串最短长度。
//那么起始值 dp[i][0]=i; dp[0][i] =i;
//也就是包含空串和一个strx字符串作为子序列的答案字符串最短长度自然就是那个strx字符串的长度
for (int i = 0; i < s1.size(); i++) { dp[i][0] = i; }
for (int j = 0; j < s2.size(); j++) { dp[0][j] = j; }
//状态转移
for (int i = 1; i < s1.size(); i++) {
for (int j = 1; j < s2.size(); j++) {
if (s1[i] == s2[j]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
else {
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1;
}
}
}
//由dp推导出字符串
string ans;
int a = s1.size() - 1; int b = s2.size() - 1;
//由于dp[i][j] 与dp[i-1][j-1] dp[i-1][j] dp[i][j-1]的变化
//确定是选择哪个字符串中的哪个字母
while (a > 0 && b > 0) {
if (s1[a] == s2[b]) {
ans += s1[a];
a--; b--;
}
else if (dp[a][b] == dp[a - 1][b] + 1) {
ans += s1[a]; a--;
}
else if (dp[a][b] == dp[a][b - 1] + 1) {
ans += s2[b]; b--;
}
}
//由于是从后到前选择的 所以需要翻转一下字符串
reverse(ans.begin(), ans.end());
//两个字符串有一个全部添加了 那么把剩余的另一个字符串全部添加到答案前面即可
if (a > 0) {
ans = s1.substr(1,a) + ans;
}
if (b > 0) {
ans = s2.substr(1,b) + ans;
}
return ans;
}
};
第二种方案 编辑距离
dp[i][j] 表示往str1[1~i]的部分添加字母 使得其子序列能包含str2[1~j]的组合, 所添加操作的最小次数
那么起始值 dp[i][0]=0; str1[1~i]包含空字符串为子序列,所以无须操作,操作次数为0
dp[0][i] =i; 如果str1为空字符串 如果想让其子序列包含str2[1~j] 那么需要添加j个字母,操作次数为j.
从dp中推导出正确的字符串和上面的基本一致,不在重复描述了
代码如下
class Solution {
public:
vector<vector<int>> dp;
string shortestCommonSupersequence(string s1, string s2) {
dp.resize(1010, vector<int>(1010, 0x3f3f3f3f));//初始化dp
//字符串插入字符 dp从1索引开始 避免考虑边界问题
s1.insert(s1.begin(), '#'); s2.insert(s2.begin(), '@');
//初始化dp起始值
//dp[i][j] 表示往str1[1~i]的部分添加字母 使得其子序列能包含str2[1~j]的组合,
//所添加操作的最小次数
//那么起始值 dp[i][0]=0; str1[1~i]包含空字符串为子序列,所以无须操作,操作次数为0
//dp[0][i] =i; 如果str1为空字符串 如果想让其子序列包含str2[1~j]
//那么需要添加j个字母,操作次数为j.
for (int i = 0; i < s1.size(); i++) dp[i][0] = 0;
for (int i = 0; i < s1.size(); i++) dp[0][i] = i;
//转移方程 注意两个字母不同时候的转移。
//往str1[1~i-1]的部分添加字母 使得其子序列能包含str2[1~j]的组合,
//所添加操作的最小次数就等于
//往str1[1~i]的部分添加字母 使得其子序列能包含str2[1~j]的组合,
//所添加操作的最小次数
for (int i = 1; i < s1.size(); i++) {
for (int j = 1; j < s2.size(); j++) {
if (s1[i] == s2[j]) {
dp[i][j] = dp[i - 1][j - 1];
}
else {
dp[i][j] = min(dp[i][j - 1]+1, dp[i - 1][j]);
}
}
}
//从dp倒推出答案字符串
string ans;
int a = s1.size() - 1; int b = s2.size() - 1;
while (a > 0 && b > 0) {
if (s1[a] == s2[b] && dp[a][b]==dp[a-1][b-1]) {
ans += s1[a]; a--; b--;
}
else if (dp[a][b] == dp[a][b - 1] + 1) {
ans += s2[b]; b--;
}
else {
ans += s1[a]; a--;
}
}
reverse(ans.begin(), ans.end());
if (a > 0) {
ans = s1.substr(1, a) + ans;
}
if (b > 0) {
ans = s2.substr(1, b) + ans;
}
return ans;
}
};
``
[我的视频题解空间](https://space.bilibili.com/18508846)
标签:1092,s2,s1,最短,ans,字符串,Leetcocde,str1,dp
From: https://www.cnblogs.com/itdef/p/17990811