| 
 | 1 | +### 题目描述  | 
 | 2 | + | 
 | 3 | +这是 LeetCode 上的 **[1092. 最短公共超序列](https://leetcode.cn/problems/shortest-common-supersequence/solution/by-ac_oier-s346/)** ,难度为 **困难**。  | 
 | 4 | + | 
 | 5 | +Tag : 「序列 DP」、「LCS」、「最长上升子序列」、「动态规划」、「构造」、「双指针」  | 
 | 6 | + | 
 | 7 | + | 
 | 8 | + | 
 | 9 | +给出两个字符串 `str1` 和 `str2`,返回同时以 `str1` 和 `str2` 作为子序列的最短字符串。如果答案不止一个,则可以返回满足条件的任意一个答案。  | 
 | 10 | + | 
 | 11 | +(如果从字符串 `T` 中删除一些字符(也可能不删除,并且选出的这些字符可以位于 `T` 中的 任意位置),可以得到字符串 `S`,那么 `S` 就是 `T` 的子序列)  | 
 | 12 | + | 
 | 13 | +示例:  | 
 | 14 | +```  | 
 | 15 | +输入:str1 = "abac", str2 = "cab"  | 
 | 16 | + | 
 | 17 | +输出:"cabac"  | 
 | 18 | + | 
 | 19 | +解释:  | 
 | 20 | +str1 = "abac" 是 "cabac" 的一个子串,因为我们可以删去 "cabac" 的第一个 "c"得到 "abac"。   | 
 | 21 | +str2 = "cab" 是 "cabac" 的一个子串,因为我们可以删去 "cabac" 末尾的 "ac" 得到 "cab"。  | 
 | 22 | +最终我们给出的答案是满足上述属性的最短字符串。  | 
 | 23 | +```  | 
 | 24 | + | 
 | 25 | +提示:  | 
 | 26 | +* 1ドル <= str1.length, str2.length <= 1000$  | 
 | 27 | +* `str1` 和 `str2` 都由小写英文字母组成。  | 
 | 28 | + | 
 | 29 | +---  | 
 | 30 | + | 
 | 31 | +### LCS 求具体方案 + 构造  | 
 | 32 | + | 
 | 33 | +为了方便,我们令 `str1` 为 `s1`,`str2` 为 `s2`,并将两者长度记为 $n$ 和 $m$。  | 
 | 34 | + | 
 | 35 | +容易想到最终的方案必然是由三部分组成:两字符串的公共子序列(且必然是最长公共子序列)+ 两者特有的字符部分。  | 
 | 36 | + | 
 | 37 | +基于此,我们可以先使用对两者求 `LCS`,并在求具体方案时遵循:属于最长公共子序列的字符只添加一次,而特有于 `s1` 或 `s2` 的字符则独自添加一次。  | 
 | 38 | + | 
 | 39 | +求解 `LCS` 部分我们定义 **$f[i][j]$ 代表考虑 $s1$ 的前 $i$ 个字符、考虑 $s2$ 的前 $j$ 的字符,形成的最长公共子序列长度(为了方便,令下标从 1ドル$ 开始)。**  | 
 | 40 | + | 
 | 41 | +当有了「状态定义」之后,基本上「转移方程」就是呼之欲出:  | 
 | 42 | + | 
 | 43 | +* `s1[i]==s2[j]` : $f[i][j]=f[i-1][j-1]+1$。代表**必然使用 $s1[i]$ 与 $s2[j]$ 时** `LCS` 的长度。  | 
 | 44 | +* `s1[i]!=s2[j]` : $f[i][j]=max(f[i-1][j], f[i][j-1])$。代表**必然不使用 $s1[i]$(但可能使用$s2[j]$)时** 和 **必然不使用 $s2[j]$(但可能使用$s1[i]$)时** `LCS` 的长度。  | 
 | 45 | + | 
 | 46 | +> **不了解 LCS 的同学可以看前置 🧀 : [LCS 模板题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247492097&idx=1&sn=f51f29d86df809d8ac43a40a1369b3d6)**  | 
 | 47 | + | 
 | 48 | +当预处理出动规数组 `f` 之后,我们使用「双指针」和「通用 `DP` 求具体方案」的做法进行构造:使用 `i` 变量指向 `s1` 的尾部(即起始有 $i = n$),使用 `j` 变量指向 `s2` 的尾部(即起始有 $j = m$),根据 `i` 和 `j` 当前所在位置以及 $f[i][j]$ 从何值转移而来:  | 
 | 49 | + | 
 | 50 | +1. 若 `i` 或 `j` 其一走完(`i = 0` 或 `j = 0`),将剩余字符追加到答案中;  | 
 | 51 | +2. 当 $f[i][j] = f[i - 1][j - 1] + 1$ 且 $s1[i] = s2[j]$ 时(可简化为 $s1[i] = s2[j]$ 判断),此时 `i` 指向的字符和 `j` 指向的字符为相同,且为 `LCS` 中的字符,将其追加到具体方案,并让 `i` 和 `j` 同时后移;  | 
 | 52 | +3. 当 $f[i][j] = f[i - 1][j],ドル将 `s1[i]` 追加到答案中,令 `i` 后移;  | 
 | 53 | +4. 当 $f[i][j] = f[i][j - 1],ドル将 `s2[j]` 追加到答案中,令 `j` 后移。  | 
 | 54 | + | 
 | 55 | +当条件 `3` 和 `4` 同时满足时,由于只需要输出任一具体方案,我们任取其一即可。  | 
 | 56 | + | 
 | 57 | +最后,由于我们是从后往前进行构造,在返回时需要再进行一次翻转。  | 
 | 58 | + | 
 | 59 | +Java 代码:  | 
 | 60 | +```Java  | 
 | 61 | +class Solution {  | 
 | 62 | + public String shortestCommonSupersequence(String str1, String str2) {  | 
 | 63 | + int n = str1.length(), m = str2.length();  | 
 | 64 | + str1 = " " + str1; str2 = " " + str2;  | 
 | 65 | + char[] s1 = str1.toCharArray(), s2 = str2.toCharArray();  | 
 | 66 | + int[][] f = new int[n + 10][m + 10];  | 
 | 67 | + for (int i = 1; i <= n; i++) {  | 
 | 68 | + for (int j = 1; j <= m; j++) {  | 
 | 69 | + if (s1[i] == s2[j]) f[i][j] = f[i - 1][j - 1] + 1;  | 
 | 70 | + else f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);  | 
 | 71 | + }  | 
 | 72 | + }  | 
 | 73 | + StringBuilder sb = new StringBuilder();  | 
 | 74 | + int i = n, j = m;  | 
 | 75 | + while (i > 0 || j > 0) {  | 
 | 76 | + if (i == 0) sb.append(s2[j--]);  | 
 | 77 | + else if (j == 0) sb.append(s1[i--]);  | 
 | 78 | + else {  | 
 | 79 | + if (s1[i] == s2[j]) {  | 
 | 80 | + sb.append(s1[i]);  | 
 | 81 | + i--; j--;  | 
 | 82 | + } else if (f[i][j] == f[i - 1][j]) {  | 
 | 83 | + sb.append(s1[i--]);  | 
 | 84 | + } else {  | 
 | 85 | + sb.append(s2[j--]);  | 
 | 86 | + }  | 
 | 87 | + }  | 
 | 88 | + }  | 
 | 89 | + return sb.reverse().toString();  | 
 | 90 | + }  | 
 | 91 | +}  | 
 | 92 | +```  | 
 | 93 | +TypeScript 代码:  | 
 | 94 | +```TypeScript  | 
 | 95 | +function shortestCommonSupersequence(s1: string, s2: string): string {  | 
 | 96 | + const n = s1.length, m = s2.length  | 
 | 97 | + s1 = " " + s1; s2 = " " + s2  | 
 | 98 | + const f = new Array<Array<number>>()  | 
 | 99 | + for (let i = 0; i < n + 10; i++) f.push(new Array<number>(m + 10).fill(0))  | 
 | 100 | + for (let i = 1; i <= n; i++) {  | 
 | 101 | + for (let j = 1; j <= m; j++) {  | 
 | 102 | + if (s1[i] == s2[j]) f[i][j] = f[i - 1][j - 1] + 1  | 
 | 103 | + else f[i][j] = Math.max(f[i - 1][j], f[i][j - 1])  | 
 | 104 | + }  | 
 | 105 | + }  | 
 | 106 | + let ans = ""  | 
 | 107 | + let i = n, j = m  | 
 | 108 | + while (i > 0 || j > 0) {  | 
 | 109 | + if (i == 0) ans += s2[j--]  | 
 | 110 | + else if (j == 0) ans += s1[i--]  | 
 | 111 | + else {  | 
 | 112 | + if (s1[i] == s2[j]) {  | 
 | 113 | + ans += s1[i]  | 
 | 114 | + i--; j--  | 
 | 115 | + } else if (f[i][j] == f[i - 1][j]) {  | 
 | 116 | + ans += s1[i--]  | 
 | 117 | + } else {  | 
 | 118 | + ans += s2[j--]  | 
 | 119 | + }  | 
 | 120 | + }  | 
 | 121 | + }  | 
 | 122 | + return ans.split('').reverse().join('')  | 
 | 123 | +};  | 
 | 124 | +```  | 
 | 125 | +Python 代码:  | 
 | 126 | +```Python  | 
 | 127 | +class Solution:  | 
 | 128 | + def shortestCommonSupersequence(self, s1: str, s2: str) -> str:  | 
 | 129 | + n, m = len(s1), len(s2)  | 
 | 130 | + s1 = " " + s1  | 
 | 131 | + s2 = " " + s2  | 
 | 132 | + f = [[0] * (m + 10) for _ in range(n + 10)]  | 
 | 133 | + for i in range(1, n + 1):  | 
 | 134 | + for j in range(1, m + 1):  | 
 | 135 | + f[i][j] = f[i - 1][j - 1] + 1 if s1[i] == s2[j] else max(f[i - 1][j], f[i][j - 1])  | 
 | 136 | + ans = ""  | 
 | 137 | + i, j = n, m  | 
 | 138 | + while i > 0 or j > 0:  | 
 | 139 | + if i == 0:  | 
 | 140 | + ans += s2[j]  | 
 | 141 | + j -= 1  | 
 | 142 | + elif j == 0:  | 
 | 143 | + ans += s1[i]  | 
 | 144 | + i -= 1  | 
 | 145 | + else:  | 
 | 146 | + if s1[i] == s2[j]:  | 
 | 147 | + ans += s1[i]  | 
 | 148 | + i -= 1  | 
 | 149 | + j -= 1  | 
 | 150 | + elif f[i][j] == f[i - 1][j]:  | 
 | 151 | + ans += s1[i]  | 
 | 152 | + i -= 1  | 
 | 153 | + else:  | 
 | 154 | + ans += s2[j]  | 
 | 155 | + j -= 1  | 
 | 156 | + return ans[::-1]  | 
 | 157 | +```  | 
 | 158 | +* 时间复杂度:`LCS` 复杂度为 $O(n \times m)$;构造答案复杂度为 $O(n \times m)$。整体复杂度为 $O(n \times m)$  | 
 | 159 | +* 空间复杂度:$O(n \times m)$  | 
 | 160 | + | 
 | 161 | +---  | 
 | 162 | + | 
 | 163 | +### 最后  | 
 | 164 | + | 
 | 165 | +这是我们「刷穿 LeetCode」系列文章的第 `No.1092` 篇,系列开始于 2021年01月01日,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。  | 
 | 166 | + | 
 | 167 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。  | 
 | 168 | + | 
 | 169 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。  | 
 | 170 | + | 
 | 171 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。  | 
 | 172 | + | 
0 commit comments