两个字符串的删除操作
问题描述
给定两个单词 word1 和 word2,返回使得 word1 和 word2 相同所需的最小步数。每步可以删除任意一个字符串中的一个字符。
示例:
输入: word1 = "sea", word2 = "eat"
输出: 2
解释: 第一步将 "sea" 变为 "ea",第二步将 "eat" 变为 "ea"
算法思路
这个问题可以转化为最长公共子序列(LCS)问题:
- 两个字符串的最长公共子序列是希望保留的部分
- 需要删除的字符数 = word1.length() + word2.length() - 2 * LCS长度
动态规划思路:
- 定义
dp[i][j]表示word1的前i个字符和word2的前j个字符的最长公共子序列长度 - 状态转移方程:
- 如果
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])
- 如果
- 最终结果:
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
}
关键点
-
问题转化:
- 最小删除操作数 = 总长度 - 2 × 最长公共子序列长度
- 保留LCS,删除其余所有字符
-
动态规划状态定义:
dp[i][j]表示前缀的LCS长度,不是删除次数- 最终结果需要通过公式转换
-
空间优化:
- 使用
滚动数组,只保存当前行和上一行的状态 - 通过
prev变量保存对角线上的值dp[i-1][j-1]
- 使用
-
边界处理:
- 空字符串的LCS长度为0
- 相同字符串的删除次数为0
常见问题
-
为什么是减去2倍的LCS长度?
- LCS在两个字符串中各出现一次,所以总共要保留
2 × LCS个字符 - 需要删除的字符数 = 总字符数 - 保留的字符数 =
m + n - 2 × LCS
- LCS在两个字符串中各出现一次,所以总共要保留
-
如何理解状态转移方程?
- 当字符相同时,可以将当前字符加入LCS,长度加1
- 当字符不同时,选择删除其中一个字符,取两种方案的最大LCS
-
空间优化中prev变量的作用是什么?
- 在一维数组中,
dp[j-1]已经被更新为当前行的值 prev保存了上一行dp[j-1]的原始值,即dp[i-1][j-1]
- 在一维数组中,
-
这个问题和编辑距离有什么区别?
- 编辑距离允许插入、删除、替换操作
- 本题只允许删除操作,因此可以转化为LCS问题
- 如果只允许删除操作,编辑距离 =
m + n - 2 × LCS
两个字符串的删除操作

被折叠的 条评论
为什么被折叠?



