两个字符串的删除操作 图解题解
这道题到底在问什么
- word1
- "sea"
- word2
- "eat"
- 输出
- 2
- 解释
- sea 删掉 s 变 ea,eat 删掉 t 变 ea,两步后都成 "ea"
先想最直接的笨办法
如果硬枚举「这个字符删不删」,两个单词加起来 n 个字符就有 2ⁿ 种删法,根本算不过来。得换个角度:与其想删什么,不如想留什么。(动画第 3 步)
最优解:为什么这么做
一句话答案:LeetCode 583 两个字符串的删除操作:要删得最少就让两串保留的公共部分尽量长,即求最长公共子序列 LCS,答案 = m+n−2L,填二维 dp 表,时间 O(m×n)、空间 O(m×n)。
两个单词删到相同,最少删几个字符
给两个单词 word1 和 word2,每步能从任意一个里删掉一个字符,问最少删几次能让它们完全相同。例子 word1="sea"、word2="eat" 答案 2:sea 删开头的 s、eat 删结尾的 t,两步后都剩 "ea"。
为什么挨个试「删不删」根本试不完
最直接的想法是对每个字符试「删还是不删」。两串共 m+n 个字符、每个两种选择,就有 2^(m+n) 种删法(大 O 记号里叫指数级),字符稍多就枚举不完,且前半截大量重叠被反复重算。
为什么删到只剩公共子序列最省,答案又为何是 m+n−2L
不管怎么删,两串最后剩的是同一个串,它同时是 word1 和 word2 的子序列(按原顺序挑字符、允许中间跳过)。想删得最少,就让这个公共串尽量长,即求最长公共子序列(英文缩写 LCS)。
设 LCS 长度为 L。word1 有 m 个字符、删到只剩 L 个要删 m−L 个,word2 有 n 个要删 n−L 个,相加得删除数 = m+n−2L。L 减两次,因为那 L 个公共字符两串各留一份。求出 L 即可。
dp 表怎么填,字母相等和不等分别接哪个邻居
开一张 (m+1)×(n+1) 的表,dp[i][j] 表示只看 word1 前 i 个、word2 前 j 个字母时公共子序列最长多少。第 0 行列代表空串全填 0,方便起步。
填 dp[i][j] 时比较 word1 第 i 个和 word2 第 j 个字母(下标 word1[i−1]、word2[j−1],比 dp 表错开一位)。相等就把这对接进公共子序列,长度 = 左上角 dp[i−1][j−1] 加 1;不等就配不上,只能丢掉其中一个字母,取上方 dp[i−1][j]、左方 dp[i][j−1] 中较大的。
拿 "sea" 和 "eat" 亲手把整张表填一遍
m=n=3,第 0 行列全 0。第一行 s 与 e、a、t 都不等取上左大值全 0。第二行 e 撞 word2 的 e,左上角 0 加 1 得 dp[2][1]=1;往右 e 和 a、t 不等取左边 1,dp[2][2]=dp[2][3]=1。第三行 a 和 e 不等取上方 1 得 dp[3][1]=1;a 撞 a,左上角 dp[2][1]=1 加 1 得 dp[3][2]=2,公共子序列长成 "ea";末格 a 和 t 不等取上方 1、左方 2 得 dp[3][3]=2。
右下角 dp[3][3]=2 就是 L。代回公式,答案 = m+n−2L = 3+3−2×2 = 2,和题目输出对上:word1 删 s、word2 删 t,各 1 个。
填满这张表要多久,子序列可别当成子串
表有 (m+1)×(n+1) 格、每格一次比较取值,时间 O(m×n);二维表占 O(m×n) 空间,又因每格只看上一行和本行左边,可用滚动数组(只留最近一两行、循环覆盖旧值)把空间压到 O(n)。
两个坑最常见。一是把子序列求成最长公共子串——子串要连续、子序列顺序对即可跳过,转移不同,当子串会算出更小的公共长度、删除步数偏大。二是答案写成 m+n−L:漏一个 L,公共部分两边各留一份要减两次。
▶ 动画逐步走查(共 28 步)——想跟着动画一帧帧对照就展开
- 3如果硬枚举「这个字符删不删」,两个单词加起来 n 个字符就有 2ⁿ 种删法,根本算不过来。得换个角度:与其想删什么,不如想留什么。
- 4不管怎么删,最后两边剩下的是同一个串,它必然同时是 word1 和 word2 的子序列。想删得最少,就要让这个公共部分尽量长——这正是「最长公共子序列(LCS)」。
- 5word1 长 m,要删到只剩公共部分得删 m−L 个;word2 长 n,删 n−L 个。两边相加 = m + n − 2L。问题就缩成一句话:求出最长公共子序列长度 L。
- 6找它们的最长公共子序列上行是 word1="sea",下行是 word2="eat"。肉眼能看出公共子序列是 "ea"(在两串里都按顺序出现),长度 2。下面用 DP 表把这个 2 一格一格算出来。
- 7我们开一张 (m+1)×(n+1) 的表。dp[i][j] 表示:只看 word1 的前 i 个字母、word2 的前 j 个字母,它们最长的公共子序列有多长。多出来的第 0 行第 0 列表示「空串」,方便起步。
- 8看 word1 第 i 个、word2 第 j 个字母:相等就把这对配进公共子序列,长度 = 左上角 +1;不等就只能从「丢掉其中一个字母」的两种局面里挑更长的——也就是上方和左方取大。
- 9整张表待填这是 4×4 的 dp 表。最上一行、最左一列的表头是两个单词的字母,ε 是空串占位。每个格子 dp[i][j] 等会儿都会填上一个数。先把地基(第 0 行、第 0 列)铺好。
- 10空串 vs 空串 = 0先填最左上角 dp[0][0]:两边都取空串(ε),公共子序列长度自然是 0。这是整张表的起点。
- 11空串 vs "e" = 0第 0 行代表「word1 是空串」。空串和 "e" 比,没有公共字符,0。继续往右填。
- 12空串 vs 任何前缀 = 0第 0 行剩下两格同理填 0——空串和「ea」「eat」都没有公共字符。整条第 0 行铺好了。
- 13"s" vs 空串 = 0再填第 0 列「word2 是空串」的情形:"s" 和空串比,0。这一列也得全是 0。
- 14任何前缀 vs 空串 = 0第 0 列剩下两格也填 0。地基(第 0 行 + 第 0 列)全部铺完,接下来从左上角的内部格子开始,一行一行往下填真正的值。
- 15字符不等 → 取上、左较大比 word1 的 s 和 word2 的 e:不相等。那就看「去掉 s」(上方格)和「去掉 e」(左方格)谁的 LCS 长,两个都是 0,所以 dp[1][1]=0。
- 16字符不等 → 取上、左较大s 和 a 也不等,上方 0、左方 0,取大还是 0。「sea 的前 1 个」和「eat 的前 2 个」之间没有公共字母,合理。
- 17字符不等 → 取上、左较大s 跟 t 还是不等,结果 0。第一行(只含 word1 的 s)填完——s 不在 "eat" 里出现,整行都是 0。下面第二行才开始热闹。
- 18字符相等 → 左上角 +1第一次配对成功!word1 的 e 撞上 word2 的 e。把这对 e 接进公共子序列:取左上角 dp[1][0]=0 再 +1 = 1。公共子序列冒出第一个字母。
- 19字符不等 → 取上、左较大e 和 a 不等。看上方 0 和左方 1:左边那个 1 是刚配好的 e,得保留它。取大 → dp[2][2]=1。已有的公共部分不能丢。
- 20字符不等 → 取上、左较大e 跟 t 不等,继续从上(0)和左(1)取大 = 1。第二行填完:"se" 和 "eat" 的最长公共子序列长度是 1(就是那个 e)。
- 21字符不等 → 取上、左较大进第三行,word1 的 a 比 word2 的 e:不等。上方是 1(含那个 e)、左方 0,取大 = 1。把前面 e 的成果继承下来。
- 22字符相等 → 左上角 +1第二次配对成功!word1 的 a 撞上 word2 的 a。左上角 dp[2][1]=1(那里已经攒了一个 e)再 +1 = 2。公共子序列长成了 "ea"!
- 23字符不等 → 取上、左较大最后一格:a 跟 t 不等,上方 1、左方 2,取大 = 2。右下角填好,整张表完工。
- 24L = dp[3][3] = 2表格右下角 dp[3][3]=2 就是最长公共子序列长度 L。下面回溯一下,看这个 2 是哪两个字母配出来的。
- 25两次 +1 的格子串起公共子序列回溯两次「左上角 +1」发生的格子:dp[2][1] 配出 e、dp[3][2] 配出 a,串起来正是 "ea"。和一开始肉眼看到的公共子序列对上了。
- 26套上公式:word1 删 m−L=3−2=1 个(删 s),word2 删 n−L=3−2=1 个(删 t),共 2 步。两边都剩 "ea"。和题目示例的输出 2 完全对上!
- 27左:word1 要删 1 个 · 右:word2 要删 1 个把删除量画成柱子:word1 删 1 个 + word2 删 1 个 = 总共 2 次删除。本质上,每个不在公共子序列里的字母都得删掉,两边加起来就是答案。
- 28不再去试所有删法,而是填一张 m×n 的表,每格只看左、上、左上三个邻居,一次填好不回头。指数级直接降到多项式级。
- 31公共子序列那 L 个字符在两个单词里各出现一次,都要保留。所以 word1 删 (m−L)、word2 删 (n−L),L 被减了两次——一边一次。
- 33若要求连续,e 后接 a 必须紧邻如果误用「公共子串」的转移(不等就归 0),表里 e 和 a 各自孤立成 1,连不起来,会得出错误的删除步数。本题要的是 子序列:e 和 a 在两串里都按顺序出现,哪怕中间隔着别的字母也算数。
⚠️ 容易写错的地方
✗ 错:求成最长公共子串(连续)
✓ 对:求最长公共子序列(可不连续)
子序列只要顺序对,不必相邻
✗ 错:答案写成 m + n − L
✓ 对:答案是 m + n − 2L
公共部分两边各留一份,L 要减两次
✗ 错:下标 dp[i][j] 用 word[i] 比较
✓ 对:比的是 word1[i-1]、word2[j-1]
dp 多开了一圈,字符下标要错位 1
完整代码(Python / C++ / Java)
Python
class Solution:
def minDistance(self, word1, word2):
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)] # dp[i][j]=LCS
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1 # 相等 +1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) # 取大
lcs = dp[m][n]
return m + n - 2 * lcs # 删除步数C++
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.size(), n = word2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1[i - 1] == word2[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1; // 相等 +1
else
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
int lcs = dp[m][n];
return m + n - 2 * lcs; // 删除步数
}
};Java
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length(), n = word2.length();
int[][] dp = new int[m + 1][n + 1]; // dp[i][j]=LCS
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; // 相等 +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; // 删除步数
}
}复杂度
时间复杂度
O(m × n)
填满整张 (m+1)×(n+1) 表,每格 O(1)
空间复杂度
O(m × n)
二维 dp 表;可压成两行/一行滚动数组优化到 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 两个字符串的删除操作 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题和最长公共子序列 LeetCode 1143 是什么关系?+
本题的核心就是 1143。1143 直接求两个字符串的最长公共子序列长度 L,本题在它之上多一步换算:把 L 套进 m+n−2L 得到删除步数。dp 表的定义、相等接左上角加 1、不等取上左较大的转移,两题完全一样。会了 1143,这题只是最后多算一个减法。
这题和编辑距离 LeetCode 72 什么关系?+
LC72 允许插入、删除、替换三种操作求最少步数;本题只允许删除。少了「替换」,两串就只能靠各自删掉多余字符去逼近公共部分,于是退化成保留 LCS、删掉其余,等价于求 LCS 再用 m+n−2L 换算。
能不能不求 LCS,直接对删除步数做 DP?+
可以。令 dp[i][j] 表示让 word1 前 i 个、word2 前 j 个字符相同的最少删除数:两字符相等时 dp[i][j]=dp[i−1][j−1],不等时 dp[i][j]=min(dp[i−1][j], dp[i][j−1])+1,边界 dp[i][0]=i、dp[0][j]=j。算出的结果和 m+n−2L 完全一致,只是换了个记账角度。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 两个字符串的删除操作 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。