题目描述

✅ 583. 两个字符串的删除操作

image-20260928223537296

题意分析

每次可以从任意一个字符串中删除一个字符,求让两个字符串变得完全相同所需的最少操作次数。只能删除,不能替换、插入或改变剩余字符的先后顺序。

删除后共同保留的内容必须同时是两条原串的子序列,可以不连续。若某个保留字符在两串中配成一对,两边就各少删一个字符,因此问题可以转成尽量多保留这样的共同顺序。

解法:最长公共子序列 DP

核心思路

[!blue]

设原串长度为 m、n,最终保留的相同子序列长度为 L,两边分别需要删除 m - L 和 n - L 个字符,总数为 m + n - 2L。任何公共子序列都能通过删除其他字符得到,所以最大化 L,就等价于最小化删除数。

定义 dp[i][j] 为两串前 i、前 j 个字符的最长公共子序列长度。任一前缀为空时,没有可以配对的字符,状态为零。

两个前缀末尾字符相同时,可以将它们配成公共子序列的最后一对,前面的最佳选择来自两边都去掉末尾的状态,得到 dp[i - 1][j - 1] + 1。这是一对不同字符串中的位置,不能只缩短一边,否则会重复使用另一边的字符。

末尾字符不同时,它们不能同时作为最后一对,至少有一侧末尾不会用于这次最优匹配。分别考虑放弃第一串末尾、放弃第二串末尾,取 dp[i - 1][j] 与 dp[i][j - 1] 的最大值,就覆盖了所有可能。

按前缀长度递增填表,最后得到 dp[m][n]。再代入删除数公式,不能直接把公共子序列长度作为答案。

解题步骤

  1. 创建 (m + 1) × (n + 1) 的零状态表,零行和零列表示空前缀。
  2. 从长度一开始枚举两边前缀,比较 word1[i - 1] 与 word2[j - 1]。
  3. 相同则取左上角状态加一,不同则取上方和左方状态的较大值。
  4. 取最终最长公共子序列长度 L = dp[m][n]。
  5. 返回 m + n - 2L,分别扣除两边保留下来的字符数量。

代码实现

class Solution {
    // 为了删除次数最少,应当保留最长公共子序列,设长度为 lcs,答案就是 m + n - 2 * lcs。
    public int minDistance(String word1, String word2) {
        int m = word1.length();
        int n = word2.length();

        int[][] dp = new int[m + 1][n + 1];

        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    // 两边末尾配成一对,必须从同时缩短的前缀转移
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }

        int lcs = dp[m][n];

        // 每保留一个配对字符,两边各减少一次删除
        return m + n - 2 * lcs;
    }
}
func minDistance(word1 string, word2 string) int {
    // 为了删除次数最少,应当保留最长公共子序列,设长度为 lcs,答案就是 m + n - 2 * lcs。
    m, n := len(word1), len(word2)
    dp := make([][]int, m+1)
    for i := range dp {
        dp[i] = make([]int, n+1)
    }
    for i := 1; i <= m; i++ {
        for j := 1; j <= n; j++ {
            if word1[i-1] == word2[j-1] {
                // 两边末尾配成一对,必须从同时缩短的前缀转移
                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]
                }
            }
        }
    }

    lcs := dp[m][n]
    // 每保留一个配对字符,两边各减少一次删除
    return m + n - 2*lcs
}

复杂度分析

  • 时间复杂度:$O((m+1)(n+1))$,初始化并计算整张二维表,每个状态只依赖三个相邻状态。
  • 空间复杂度:$O((m+1)(n+1))$,当前实现保留全部前缀状态。

关键点总结

[!green]

  • 只删除意味着最终结果是公共子序列,保留越多就删除越少。
  • 状态下标表示前缀长度,读取当前字符时下标减一。
  • 相同末尾组成一对,不同末尾至少放弃一侧,形成两种递推分支。
  • 每个保留配对同时节省两次删除,所以总长度减去两倍 L。

易错点总结

[!yellow]

  • 把不相同字符按一次替换处理,加入了题目不允许的操作,会低估成本。
  • 相同字符从上方或左方加一,没有同时缩短两边,可能重复使用某个位置。
  • 不相同时只检查一个方向,丢掉另一侧更长的公共子序列选择。
  • 求成最长公共子串,强迫保留部分连续,不符合允许任意删除的要求。
  • 直接返回 L,或者只减去一倍 L,没有将两边各自省下的删除都算进去。

相似题目

题目 难度 关联与区别
1143. 最长公共子序列 中等 保留一条最长公共子序列后,其余字符都删除,最少删除次数为两串总长减两倍LCS长度。
72. 编辑距离 中等 原题还允许替换和插入,本题只允许删除,转移与操作成本不同。
712. 两个字符串的最小ASCII删除和 中等 用两个前缀构成二维动态规划状态;本题仅允许删除时可由公共子序列转换,该题删除代价改为字符编码之和。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2025/60502955
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!