算法题 两个字符串的删除操作

两个字符串的删除操作

两个字符串的删除操作

问题描述

给定两个单词 word1word2,返回使得 word1word2 相同所需的最小步数。每步可以删除任意一个字符串中的一个字符。

示例

输入: word1 = "sea", word2 = "eat"
输出: 2
解释: 第一步将 "sea" 变为 "ea",第二步将 "eat" 变为 "ea"

算法思路

这个问题可以转化为最长公共子序列(LCS)问题:

  • 两个字符串的最长公共子序列是希望保留的部分
  • 需要删除的字符数 = word1.length() + word2.length() - 2 * LCS长度

动态规划思路

  1. 定义 dp[i][j] 表示 word1 的前 i 个字符和 word2 的前 j 个字符的最长公共子序列长度
  2. 状态转移方程:
    • 如果 word1[i-1] == word2[j-1]dp[i][j] = dp[i-1][j-1] + 1
    • 否则:dp[i][j] = max(dp[i-1][j], dp[i][j-1])
  3. 最终结果:word1.length() + word2.length() - 2 * dp[m][n]

空间优化思路

  • 由于 dp[i][j] 只依赖于上一行和当前行的前一个状态
  • 可以使用一维数组进行空间优化

代码实现

方法一:二维动态规划

class Solution {
    /**
     * 计算使两个字符串相同所需的最小删除操作数
     * 基于最长公共子序列(LCS)的动态规划解法
     * 
     * @param word1 第一个字符串
     * @param word2 第二个字符串
     * @return 最小删除操作数
     */
    public int minDistance(String word1, String word2) {
        int m = word1.length();  // word1的长度
        int n = word2.length();  // word2的长度
        
        // dp[i][j] 表示word1前i个字符和word2前j个字符的LCS长度
        int[][] dp = new int[m + 1][n + 1];
        
        // 填充dp表
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                // 如果当前字符相同,LCS长度加1
                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]);
                }
            }
        }
        
        // 最小删除次数 = 两个字符串总长度 - 2倍LCS长度
        return m + n - 2 * dp[m][n];
    }
}

方法二:一维动态规划

class Solution {
    /**
     * 空间优化:使用一维数组减少空间复杂度
     * 
     * @param word1 第一个字符串
     * @param word2 第二个字符串
     * @return 最小删除操作数
     */
    public int minDistance(String word1, String word2) {
        int m = word1.length();
        int n = word2.length();
        
        // 确保word2是较短的字符串,优化空间使用
        if (m < n) {
            return minDistance(word2, word1);
        }
        
        // dp[j] 表示当前行第j列的LCS长度
        int[] dp = new int[n + 1];
        
        for (int i = 1; i <= m; i++) {
            int prev = 0;  // 保存dp[i-1][j-1]的值
            
            for (int j = 1; j <= n; j++) {
                int temp = dp[j];  // 保存当前dp[j],用于下一次迭代的prev
                
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    // 字符相同:dp[i][j] = dp[i-1][j-1] + 1
                    dp[j] = prev + 1;
                } else {
                    // 字符不同:dp[i][j] = max(dp[i-1][j], dp[i][j-1])
                    dp[j] = Math.max(dp[j], dp[j - 1]);
                }
                
                prev = temp;  // 更新prev为dp[i-1][j]
            }
        }
        
        return m + n - 2 * dp[n];
    }
}

算法分析

  • 时间复杂度:O(m × n)
    • 两种方法都需要遍历两个字符串的所有字符组合
  • 空间复杂度
    • 二维DP:O(m × n)
    • 一维DP:O(min(m, n)) - 通过交换确保使用较短字符串的长度
  • 方法对比
    • 二维DP:思路清晰,易于理解
    • 一维DP:空间效率更高,适合处理大字符串

算法过程

输入:word1 = "sea", word2 = "eat"

二维DP过程:

构建dp表(LCS长度):

    ""  e  a  t
""   0  0  0  0
s    0  0  0  0
e    0  1  1  1
a    0  1  2  2
  • LCS长度 = 2 (“ea”)
  • 删除次数 = 3 + 3 - 2 × 2 = 2

一维DP过程:

  • 初始:dp = [0,0,0,0]
  • i=1 (s): dp = [0,0,0,0]
  • i=2 (e): dp = [0,1,1,1]
  • i=3 (a): dp = [0,1,2,2]
  • 结果:3 + 3 - 2 × 2 = 2

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 测试用例1:标准示例
    System.out.println("Test 1: " + solution.minDistance("sea", "eat")); // 2
    
    // 测试用例2:相同字符串
    System.out.println("Test 2: " + solution.minDistance("leetcode", "etco")); // 4
    
    // 测试用例3:完全不同的字符串
    System.out.println("Test 3: " + solution.minDistance("abc", "def")); // 6
    
    // 测试用例4:一个为空字符串
    System.out.println("Test 4: " + solution.minDistance("", "abc")); // 3
    System.out.println("Test 5: " + solution.minDistance("abc", "")); // 3
    
    // 测试用例5:两个都为空
    System.out.println("Test 6: " + solution.minDistance("", "")); // 0
    
    // 测试用例6:一个字符串是另一个的子序列
    System.out.println("Test 7: " + solution.minDistance("abc", "aabbcc")); // 3
    
    // 测试用例7:长字符串
    System.out.println("Test 8: " + solution.minDistance("programming", "algorithm")); // 15
}

关键点

  1. 问题转化

    • 最小删除操作数 = 总长度 - 2 × 最长公共子序列长度
    • 保留LCS,删除其余所有字符
  2. 动态规划状态定义

    • dp[i][j] 表示前缀的LCS长度,不是删除次数
    • 最终结果需要通过公式转换
  3. 空间优化

    • 使用滚动数组,只保存当前行和上一行的状态
    • 通过 prev 变量保存对角线上的值 dp[i-1][j-1]
  4. 边界处理

    • 空字符串的LCS长度为0
    • 相同字符串的删除次数为0

常见问题

  1. 为什么是减去2倍的LCS长度?

    • LCS在两个字符串中各出现一次,所以总共要保留 2 × LCS 个字符
    • 需要删除的字符数 = 总字符数 - 保留的字符数 = m + n - 2 × LCS
  2. 如何理解状态转移方程?

    • 当字符相同时,可以将当前字符加入LCS,长度加1
    • 当字符不同时,选择删除其中一个字符,取两种方案的最大LCS
  3. 空间优化中prev变量的作用是什么?

    • 在一维数组中,dp[j-1] 已经被更新为当前行的值
    • prev 保存了上一行 dp[j-1] 的原始值,即 dp[i-1][j-1]
  4. 这个问题和编辑距离有什么区别?

    • 编辑距离允许插入、删除、替换操作
    • 本题只允许删除操作,因此可以转化为LCS问题
    • 如果只允许删除操作,编辑距离 = m + n - 2 × LCS
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值