零神:从递归到递推,教你一步步思考动态规划!
https://leetcode.cn/problems/shortest-common-supersequence/solution/cong-di-gui-dao-di-tui-jiao-ni-yi-bu-bu-auy8z/
难度困难176
给出两个字符串 str1
和 str2
,返回同时以 str1
和 str2
作为子序列的最短字符串。如果答案不止一个,则可以返回满足条件的任意一个答案。
(如果从字符串 T 中删除一些字符(也可能不删除,并且选出的这些字符可以位于 T 中的 任意位置),可以得到字符串 S,那么 S 就是 T 的子序列)
示例:
输入:str1 = "abac", str2 = "cab"
输出:"cabac"
解释:
str1 = "abac" 是 "cabac" 的一个子串,因为我们可以删去 "cabac" 的第一个 "c"得到 "abac"。
str2 = "cab" 是 "cabac" 的一个子串,因为我们可以删去 "cabac" 末尾的 "ac" 得到 "cab"。
最终我们给出的答案是满足上述属性的最短字符串。
提示:
1 <= str1.length, str2.length <= 1000
str1
和 str2
都由小写英文字母组成。题解:https://leetcode.cn/problems/shortest-common-supersequence/solution/cong-di-gui-dao-di-tui-jiao-ni-yi-bu-bu-auy8z/
class Solution {
/**
考虑从后往前构造答案,答案的最后一个字母是什么?要么是s的最后一个字母,要么是t的最后一个字母
如果s[n]和t[n]的最后一个字母不同,
如果答案最后一个字母是s的,问题变成构造s[n-1]和t[n]的答案
如果答案最后一个字母是t的,问题变成构造s[n]和t[n-1]的答案
如果s[n]和t[n]的最后一个字母相同
问题变成构造s[n-1]和t[n-1]的答案
边界条件:
如果s是空串,答案为t
如果t是空串,答案为s
*/
public String shortestCommonSupersequence(String s, String t) {
if(s.isEmpty()) return t; // s 是空串,返回剩余的 t
if(t.isEmpty()) return s; // t 是空串,返回剩余的 s
String s1 = s.substring(0, s.length() - 1);
String t1 = t.substring(0, t.length() - 1);
char x = s.charAt(s.length() - 1);
char y = t.charAt(t.length() - 1);
if(x == y) {// 最短公共超序列一定包含 x
return shortestCommonSupersequence(s1, t1) + x;
}
String ans1 = shortestCommonSupersequence(s1, t);
String ans2 = shortestCommonSupersequence(s, t1);
// 取 ans1 和 ans2 中更短的组成答案
if(ans1.length() < ans2.length()){
return ans1 + x;
}
return ans2 + y;
}
}
class Solution {
/**
考虑从后往前构造答案,答案的最后一个字母是什么?要么是s的最后一个字母,要么是t的最后一个字母
如果s[n]和t[n]的最后一个字母不同,
如果答案最后一个字母是s的,问题变成构造s[n-1]和t[n]的答案
如果答案最后一个字母是t的,问题变成构造s[n]和t[n-1]的答案
如果s[n]和t[n]的最后一个字母相同
问题变成构造s[n-1]和t[n-1]的答案
边界条件:
如果s是空串,答案为t
如果t是空串,答案为s
*/
private String s, t;
private String[][] memo; // [i][j]表示s前i个字母,t前j个字母构造的最大答案
public String shortestCommonSupersequence(String s, String t) {
this.s = s;
this.t = t;
memo = new String[s.length()][t.length()];
return dfs(s.length()-1, t.length()-1);
}
public String dfs(int i, int j){
if(i < 0) return t.substring(0, j+1); // s 是空串,返回剩余的 t
if(j < 0) return s.substring(0, i+1); // t 是空串,返回剩余的 s
if(memo[i][j] != null) return memo[i][j]; // 避免重复计算
if(s.charAt(i) == t.charAt(j)){
return memo[i][j] = dfs(i-1, j-1) + s.charAt(i); // 最短公共超序列一定包含 x
}
String ans1 = dfs(i-1, j);
String ans2 = dfs(i, j-1);
// 取 ans1 和 ans2 中更短的组成答案
if(ans1.length() < ans2.length()){
return memo[i][j] = ans1 + s.charAt(i);
}
return memo[i][j] = ans2 + t.charAt(j);
}
}
如果只求最短公共超序列的长度,那么递归返回的是一个整数而不是字符串,这样就只需要 O(nm)
的时间和空间,这是可以接受的。
class Solution {
/**
考虑从后往前构造答案,答案的最后一个字母是什么?要么是s的最后一个字母,要么是t的最后一个字母
如果s[n]和t[n]的最后一个字母不同,
如果答案最后一个字母是s的,问题变成构造s[n-1]和t[n]的答案
如果答案最后一个字母是t的,问题变成构造s[n]和t[n-1]的答案
如果s[n]和t[n]的最后一个字母相同
问题变成构造s[n-1]和t[n-1]的答案
边界条件:
如果s是空串,答案为t
如果t是空串,答案为s
*/
private String s, t;
private int[][] memo; // [i][j]表示s前i个字母,t前j个字母构造的最大答案
public String shortestCommonSupersequence(String s, String t) {
this.s = s;
this.t = t;
memo = new int[s.length()][t.length()];
return makeAns(s.length()-1, t.length()-1);
}
// dfs(i,j) 返回 s 的前 i 个字母和 t 的前 j 个字母的最短公共超序列的长度
private int dfs(int i, int j) {
if (i < 0) return j + 1; // s 是空串,返回剩余的 t 的长度
if (j < 0) return i + 1; // t 是空串,返回剩余的 s 的长度
if (memo[i][j] > 0) return memo[i][j]; // 避免重复计算 dfs 的结果
if (s.charAt(i) == t.charAt(j)) // 最短公共超序列一定包含 s[i]
return memo[i][j] = dfs(i - 1, j - 1) + 1;
return memo[i][j] = Math.min(dfs(i - 1, j), dfs(i, j - 1)) + 1;
}
// makeAns(i,j) 返回 s 的前 i 个字母和 t 的前 j 个字母的最短公共超序列
// 看上去和 dfs 没啥区别,但是末尾的递归是 if-else
// makeAns(i-1,j) 和 makeAns(i,j-1) 不会都调用
// 所以 makeAns 的递归树仅仅是一条链
private String makeAns(int i, int j) {
if (i < 0) return t.substring(0, j + 1); // s 是空串,返回剩余的 t
if (j < 0) return s.substring(0, i + 1); // t 是空串,返回剩余的 s
if (s.charAt(i) == t.charAt(j)) // 最短公共超序列一定包含 s[i]
return makeAns(i - 1, j - 1) + s.charAt(i);
// 如果下面 if 成立,说明上面 dfs 中的 min 取的是 dfs(i - 1, j)
// 说明 dfs(i - 1, j) 对应的公共超序列更短
// 那么就在 makeAns(i - 1, j) 的结果后面加上 s[i]
// 否则说明 dfs(i, j - 1) 对应的公共超序列更短
// 那么就在 makeAns(i, j - 1) 的结果后面加上 t[j]
if (dfs(i, j) == dfs(i - 1, j) + 1)
return makeAns(i - 1, j) + s.charAt(i);
return makeAns(i, j - 1) + t.charAt(j);
}
}
class Solution {
public String shortestCommonSupersequence(String str1, String str2) {
// f[i+1][j+1] 表示 s 的前 i 个字母和 t 的前 j 个字母的最短公共超序列的长度
char[] s = str1.toCharArray(), t = str2.toCharArray();
int n = s.length, m = t.length;
var f = new int[n + 1][m + 1];
for (int j = 1; j < m; ++j) f[0][j] = j; // 递归边界
for (int i = 1; i < n; ++i) f[i][0] = i; // 递归边界
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
if (s[i] == t[j]) // 最短公共超序列一定包含 s[i]
f[i + 1][j + 1] = f[i][j] + 1;
else // 取更短的组成答案
f[i + 1][j + 1] = Math.min(f[i][j + 1], f[i + 1][j]) + 1;
int na = f[n][m];
var ans = new char[na];
for (int i = n - 1, j = m - 1, k = na - 1; ; ) {
if (i < 0) { // s 是空串,剩余的 t 就是最短公共超序列
System.arraycopy(t, 0, ans, 0, j + 1);
break; // 相当于递归边界
}
if (j < 0) { // t 是空串,剩余的 s 就是最短公共超序列
System.arraycopy(s, 0, ans, 0, i + 1);
break; // 相当于递归边界
}
if (s[i] == t[j]) { // 公共超序列一定包含 s[i]
ans[k--] = s[i--]; // 倒着填 ans
--j; // 相当于继续递归 makeAns(i - 1, j - 1)
} else if (f[i + 1][j + 1] == f[i][j + 1] + 1)
ans[k--] += s[i--]; // 相当于继续递归 makeAns(i - 1, j)
else
ans[k--] += t[j--]; // 相当于继续递归 makeAns(i, j - 1)
}
return new String(ans);
}
}