|
| 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