两个字符串的最小 ASCII 删除和 图解题解
这道题到底在问什么
- 输入
- s1 = "sea", s2 = "eat"
- 输出
- 231(删掉 s1 的 s=115 和 s2 的 t=116,两边都剩 "ea")
- 输入
- s1 = "delete", s2 = "leet"
- 输出
- 403(保留公共子序列 "let",删掉 s1 的 d、e、e 和 s2 多出的 e)
最优解:一步一步想明白
- 3一句话套路:相同就抄左上角,不同就删一个、取较小。下面逐格填表套这条规则。
- 4先看这张表的骨架:列头是 s2「eat」的字符、行头是 s1「sea」的字符,左上再各加一行一列代表「空串」。dp[i][j] 就是「s1 前 i 个 与 s2 前 j 个相等」的最小删除代价,现在全是「·」表示还没算。
- 5先建一张 (m+1)×(n+1) 的表。左上角 dp[0][0]=0:两个都是空串,本来就相等,删 0 个、代价 0(紫格)。
- 6第 0 列:s2 是空串,要让 s1 前 1 个字符也变空,只能全删。dp[1][0] = 上一格 0 再加上 "s" 的 ASCII 115 = 115(紫格,蓝格是它依赖的上一行)。
- 7第 0 列:s2 是空串,要让 s1 前 2 个字符也变空,只能全删。dp[2][0] = 上一格 115 再加上 "e" 的 ASCII 101 = 216(紫格,蓝格是它依赖的上一行)。
- 8第 0 列:s2 是空串,要让 s1 前 3 个字符也变空,只能全删。dp[3][0] = 上一格 216 再加上 "a" 的 ASCII 97 = 313(紫格,蓝格是它依赖的上一行)。
- 9第 0 行:s1 是空串,要让 s2 前 1 个字符变空,同样全删。dp[0][1] = 左边一格 0 加 "e" 的 ASCII 101 = 101(紫格,蓝格是它依赖的左邻)。
- 10第 0 行:s1 是空串,要让 s2 前 2 个字符变空,同样全删。dp[0][2] = 左边一格 101 加 "a" 的 ASCII 97 = 198(紫格,蓝格是它依赖的左邻)。
- 11第 0 行:s1 是空串,要让 s2 前 3 个字符变空,同样全删。dp[0][3] = 左边一格 198 加 "t" 的 ASCII 116 = 314(紫格,蓝格是它依赖的左邻)。
- 12比较 "s" 和 "e":不同,必须删掉其中一个。方案一删 s1 的 "s":上方格 dp[0][1]=101 加 115 = 216。方案二删 s2 的 "e":左边格 dp[1][0]=115 加 101 = 216。两个方案代价相同,都是 216;平局时任选其一都不影响最优值,本格填 216,填入紫格。
- 13比较 "s" 和 "a":不同,必须删掉其中一个。方案一删 s1 的 "s":上方格 dp[0][2]=198 加 115 = 313。方案二删 s2 的 "a":左边格 dp[1][1]=216 加 97 = 313。两个方案代价相同,都是 313;平局时任选其一都不影响最优值,本格填 313,填入紫格。
- 14比较 "s" 和 "t":不同,必须删掉其中一个。方案一删 s1 的 "s":上方格 dp[0][3]=314 加 115 = 429。方案二删 s2 的 "t":左边格 dp[1][2]=313 加 116 = 429。两个方案代价相同,都是 429;平局时任选其一都不影响最优值,本格填 429,填入紫格。
- 15比较 "e"(s1 第 2 个)和 "e"(s2 第 1 个):相同!这个字符两边都能留着、谁都不用删。直接把左上角 dp[1][0]=115 抄过来填入紫格(蓝格就是左上角)。
- 16比较 "e" 和 "a":不同,必须删掉其中一个。方案一删 s1 的 "e":上方格 dp[1][2]=313 加 101 = 414。方案二删 s2 的 "a":左边格 dp[2][1]=115 加 97 = 212。取较小 = 212(删 a更省),填入紫格。
- 17比较 "e" 和 "t":不同,必须删掉其中一个。方案一删 s1 的 "e":上方格 dp[1][3]=429 加 101 = 530。方案二删 s2 的 "t":左边格 dp[2][2]=212 加 116 = 328。取较小 = 328(删 t更省),填入紫格。
- 18比较 "a" 和 "e":不同,必须删掉其中一个。方案一删 s1 的 "a":上方格 dp[2][1]=115 加 97 = 212。方案二删 s2 的 "e":左边格 dp[3][0]=313 加 101 = 414。取较小 = 212(删 a更省),填入紫格。
- 19比较 "a"(s1 第 3 个)和 "a"(s2 第 2 个):相同!这个字符两边都能留着、谁都不用删。直接把左上角 dp[2][1]=115 抄过来填入紫格(蓝格就是左上角)。
- 20比较 "a" 和 "t":不同,必须删掉其中一个。方案一删 s1 的 "a":上方格 dp[2][3]=328 加 97 = 425。方案二删 s2 的 "t":左边格 dp[3][2]=115 加 116 = 231。取较小 = 231(删 t更省),填入紫格。
- 21表填满了。沿转移回看:s1 的 "e"(第 2 个)和 s2 的 "e"(第 1 个)相同、"a" 与 "a" 相同,它们组成保留下来的公共子序列 "ea"(蓝格);其余 "s"、"t" 被删。
- 22整张表填满,右下角 dp[3][3] = 231 就是答案:让 "sea" 和 "eat" 相等的最小删除代价。回看路径,正是删了 s1 的 "s"(115)和 s2 的 "t"(116),两边都剩 "ea",115 + 116 = 231。
- 23复盘整条思路:dp[i][j] 表示「让两个前缀相等的最小删代价」,相同抄左上角、不同删一个取较小,从左上往右下一格格推到底,右下角即答案 231。
⚠️ 容易写错的地方
✗ 错:把目标当成「删的字符个数最少」
✓ 对:目标是删的字符 ASCII 之和最少
同样删两个字符,删两个小写 a(97)远比删两个 z(122)便宜,要的是代价和不是个数
✗ 错:相同字符还去加 ASCII,或不同字符只取一个方向
✓ 对:相同就直接抄左上角、代价不变;不同就把删 s1 和删 s2 两个方案都算再取较小
相同字符两边都能保留、误加 ASCII 会凭空多算;不同字符只看一个方向会错过更省的删法
✗ 错:滚动数组直接把旧 dp[j] 覆盖进 prev 再算本格
✓ 对:先用 old 存旧 dp[j],用此刻 prev 算完本格,再令 prev = old
本格要用的「左上角」正是 prev,若提前被旧 dp[j] 覆盖,本格就会用错值
完整代码(Python / C++ / Java)
Python
class Solution:
def minimumDeleteSum(self, s1: str, s2: str) -> int:
n = len(s2)
dp = [0] * (n + 1)
for j, ch in enumerate(s2, 1):
dp[j] = dp[j-1] + ord(ch)
for a in s1:
prev = dp[0]
dp[0] += ord(a)
for j, b in enumerate(s2, 1):
old = dp[j]
if a == b:
dp[j] = prev
else:
dp[j] = min(dp[j] + ord(a), dp[j-1] + ord(b))
prev = old
return dp[n]C++
#include <algorithm>
#include <string>
#include <vector>
using namespace std;
class Solution {
public:
int minimumDeleteSum(string s1, string s2) {
int n = s2.size();
vector<int> dp(n + 1);
for (int j = 1; j <= n; ++j) dp[j] = dp[j-1] + s2[j-1];
for (char a : s1) {
int prev = dp[0];
dp[0] += a;
for (int j = 1; j <= n; ++j) {
int old = dp[j];
if (a == s2[j-1]) dp[j] = prev;
else dp[j] = min(dp[j] + (int)a, dp[j-1] + (int)s2[j-1]);
prev = old;
}
}
return dp[n];
}
};Java
import java.util.*;
class Solution {
public int minimumDeleteSum(String s1, String s2) {
int n = s2.length();
int[] dp = new int[n + 1];
for (int j = 1; j <= n; j++) dp[j] = dp[j - 1] + s2.charAt(j - 1);
for (char a : s1.toCharArray()) {
int prev = dp[0];
dp[0] += a;
for (int j = 1; j <= n; j++) {
int old = dp[j];
if (a == s2.charAt(j - 1)) dp[j] = prev;
else dp[j] = Math.min(dp[j] + a, dp[j - 1] + s2.charAt(j - 1));
prev = old;
}
}
return dp[n];
}
}复杂度
时间
O(mn)
m、n 为两串长度,每个格子 O(1) 转移,共填 m×n 个格子
空间
O(n)
滚动数组只存一行;若用完整二维表则是 O(mn)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 两个字符串的最小 ASCII 删除和 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题和最长公共子序列(LCS)是什么关系?+
本质相通。要让两串相等,留下的一定是某个公共子序列、其余全删。设两串总 ASCII 和为 S,保留的公共子序列在两个串里各留一份,所以删除代价 = S 减去「2 倍的保留公共子序列 ASCII 和」。因此「最小删除代价」等价于「最大化保留公共子序列的 ASCII 权重和」,也就是带权版的 LCS。直接做删除代价的 DP 更直接,不必先转成 LCS。
为什么这里要按 ASCII 之和而不是子序列长度来最大化?+
因为代价由字符的 ASCII 大小决定,不是数量。比如保留一个 z(122)抵得上保留一个 a(97)还多。所以等价的 LCS 必须是「ASCII 权重和最大」的版本,而不是最长版本:最长的公共子序列不一定带来最小删除代价。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 两个字符串的最小 ASCII 删除和 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。