题目描述

✅ 1092. 最短公共超序列

image-20260928225703614

题意分析

构造一个长度最短的字符串,使 str1 和 str2 都能作为它的子序列出现。允许在两个输入的字符之间穿插其他字符,但每个输入串内部的字符顺序都必须保留,不能只按字母数量合并。

要求返回构造出的字符串,而不只是最短长度。若存在多个同样短的答案,返回任意一个即可,不需要追求字典序最小。相同字符可以被两个输入共同使用,从而缩短结果。

解法:LCS 辅助重建

核心思路

[!blue]

先把两个串完整拼接,长度为 m + n。要让结果更短,就需要让两个输入共用某些字符位置。共用的位置在两个串中都必须保持相同先后顺序,因此它们一定构成公共子序列,最多只能共用最长公共子序列的长度 L。所以任何结果都至少有 m + n - L 个字符。

用后缀 LCS 表记录可以共用多少字符:dp[i][j] 表示 str1[i..) 与 str2[j..) 的最长公共子序列长度。当前字符相等时可共同使用,状态为 1 + dp[i + 1][j + 1];不等时至少要先处理其中一侧,取 dp[i + 1][j] 和 dp[i][j + 1] 的较大值。空后缀的 LCS 为零,依赖更大的下标,所以倒序填表。

重建从两个起点开始。如果当前字符相同,就输出一次并同时推进两边,共享这个位置。如果不同,就把其中一个当前字符单独输出,再推进这一侧;选择哪一侧,取决于消耗它之后能留下多少公共字符,选择对应后缀 LCS 更大的方向即可,平局时任选。

这里的“消耗”不是删除输入字符:这个字符已经写入结果,只是不与另一侧共享。每步都输出某个输入当前最前面的未处理字符,因此两串的相对顺序始终保留。沿表中最优转移恰好共用 L 次,最后长度达到 m + n - L,也就达到了前面的最短下界。

一侧耗尽后,另一侧没有可共享的对象,剩余字符必须全部原序追加。保留完整 DP 表是为了在重建时查询后续最优方向,仅有一个最终 LCS 长度不足以完成这一步。

解题步骤

  1. 创建 (m + 1) × (n + 1) 的后缀 LCS 表,末行与末列保持零。
  2. 从后向前比较两串字符,按相等取右下对角加一、不等取两个后缀最大值的规则填表。
  3. 从 i = 0、j = 0 开始构造:相等时输出一个字符并同时推进。
  4. 不等时比较 dp[i + 1][j] 与 dp[i][j + 1],输出保留更多公共字符的那一侧当前字符,并只推进这一侧。
  5. 一侧用完后追加另一侧剩余后缀,返回结果。

代码实现

class Solution {
    public String shortestCommonSupersequence(String str1, String str2) {
        int m = str1.length();
        int n = str2.length();
        int[][] dp = new int[m + 1][n + 1];

        // 后缀依赖更大的下标,按逆序填表。
        for (int i = m - 1; i >= 0; i--) {
            for (int j = n - 1; j >= 0; j--) {
                if (str1.charAt(i) == str2.charAt(j)) {
                    dp[i][j] = dp[i + 1][j + 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
                }
            }
        }

        StringBuilder answer = new StringBuilder();
        int i = 0;
        int j = 0;

        while (i < m && j < n) {
            if (str1.charAt(i) == str2.charAt(j)) {
                answer.append(str1.charAt(i));
                i++;
                j++;
            } else if (dp[i + 1][j] >= dp[i][j + 1]) {
                // 消耗这一侧后保留更多公共字符;相等时任选。
                answer.append(str1.charAt(i));
                i++;
            } else {
                answer.append(str2.charAt(j));
                j++;
            }
        }

        // 其中一串已耗尽,把剩余字符全部保留。
        answer.append(str1.substring(i));
        answer.append(str2.substring(j));

        return answer.toString();
    }
}
func shortestCommonSupersequence(str1 string, str2 string) string {
    m, n := len(str1), len(str2)
    dp := make([][]int, m+1)
    for i := range dp {
        dp[i] = make([]int, n+1)
    }

    // 后缀依赖更大的下标,按逆序填表。
    for i := m - 1; i >= 0; i-- {
        for j := n - 1; j >= 0; j-- {
            if str1[i] == str2[j] {
                dp[i][j] = dp[i+1][j+1] + 1
            } else if dp[i+1][j] >= dp[i][j+1] {
                // 消耗这一侧后保留更多公共字符;相等时任选。
                dp[i][j] = dp[i+1][j]
            } else {
                dp[i][j] = dp[i][j+1]
            }
        }
    }

    answer := make([]byte, 0, m+n)
    i, j := 0, 0
    for i < m && j < n {
        if str1[i] == str2[j] {
            answer = append(answer, str1[i])
            i++
            j++
        } else if dp[i+1][j] >= dp[i][j+1] {
            // 消耗这一侧后保留更多公共字符;相等时任选。
            answer = append(answer, str1[i])
            i++
        } else {
            answer = append(answer, str2[j])
            j++
        }
    }

    // 其中一串已耗尽,把剩余字符全部保留。
    answer = append(answer, str1[i:]...)
    answer = append(answer, str2[j:]...)
    return string(answer)
}

复杂度分析

  • 时间复杂度:O(mn + m + n)。填表处理 mn 个状态,构造过程每步至少推进一个输入下标,总长度不超过 m + n。
  • 空间复杂度:O(mn + m + n),包括完整 DP 表和用于构造结果的字符缓冲区。

关键点总结

[!green]

  • 共用字符必须形成公共子序列,给出最短长度下界 m + n - LCS。
  • LCS 表决定下一步怎样保留最多共享机会,构造结果能达到该下界。
  • 不相等时输出一侧后推进一侧,相等时两个输入共用一个输出位置。
  • 本题允许任意最短答案,分支长度相同时无需额外按字典序选择。

易错点总结

[!yellow]

  • 只按字符频次合并:忽略两个输入各自的先后顺序,不能保证它们是结果的子序列。
  • 后缀状态正序填表:依赖项位于更大的下标,必须先完成后面的状态。
  • 不等时同时推进两边:只写一个字符却消耗两个不同字符,会丢失必要内容。
  • 按LCS回溯时只输出匹配字符:那得到的是公共子序列,未共享的字符也需要写入超序列。
  • 遗漏尚未结束的一侧后缀:会使完整输入串无法作为结果的子序列。

相似题目

题目 难度 关联与区别
1143. 最长公共子序列 中等 一条LCS决定两串能共享哪些字符,围绕它合并未匹配字符可得到最短公共超序列。
583. 两个字符串的删除操作 中等 原题删除不同部分以保留共同子序列,本题合并两串的不同部分以同时容纳两串。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2023/43827621
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!