两个字符串的最小 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)
最优解:为什么这么做
一句话答案:LeetCode 712 两个字符串的最小 ASCII 删除和用二维 DP:dp[i][j] 是让两前缀相等的最小删代价,相同抄左上角、不同删 s1 或 s2 取较小,按 ASCII 加权非数量,时间 O(mn)、空间 O(n)。
删字符让两串相等,为什么代价按 ASCII 算
给两个字符串 s1、s2,只能删字符让它们变成完全一样,每删一个字符付出它的 ASCII 值(每个字符对应的数字编号,如 a=97)作代价,求最小总代价。关键是被删字符的 ASCII 之和最小、不是删的个数最少。
为什么不能先求最长公共子序列、其余全删
最直接是枚举「留哪些字符」:两串各留一个子序列(可不连续)使之相等、其余全删。可长 m、n 的串子序列有 2^m、2^n 种,指数爆炸跑不完。
留下的必是公共子序列,留最长的不就删得最少?但本题代价按 ASCII 加权:保留字符的 ASCII 权重和才决定省多少、不看个数,最长的未必最省。不如直接对删除代价做动态规划(DP,存子问题避免重算)。
dp[i][j] 存的是哪两段前缀的最小删代价
定义 dp[i][j] 为「让 s1 前 i 个字符和 s2 前 j 个字符相等的最小删除代价」,答案是 dp[m][n]。开 (m+1)×(n+1) 的表,第 0 行、第 0 列代表某串取空串:dp[0][0]=0;某串空时另一串只能全删,dp[i][0] 是 s1 前 i 个的 ASCII 前缀和,dp[0][j] 同理。
字符相同为什么抄左上角、不同为什么取两个方向的较小
算 dp[i][j] 看 s1 第 i 个字符 a、s2 第 j 个字符 b。相同时 a、b 都留、谁都不删,代价等于各砍掉这一字符后的子问题 dp[i-1][j-1],直接抄左上角、不加 ASCII。
不同时至少删一个。删 a 是 ord(a)(取 a 的 ASCII 值)加 dp[i-1][j],删 b 是 ord(b) 加 dp[i][j-1],取较小:dp[i][j]=min(dp[i-1][j]+ord(a), dp[i][j-1]+ord(b))。相同却加 ASCII 会多算,只看一个方向会漏掉更省的删法。
拿 sea 和 eat 把这张表填出 231
ASCII:s=115、e=101、a=97、t=116。边界第 0 列 dp[1..3][0]=115、216、313,第 0 行 dp[0][1..3]=101、198、314。
s 对 e、a、t 都不同,首格显式算一次:dp[1][1]=min(dp[0][1]+115, dp[1][0]+101)=216,之后压缩写 dp[1][2]=313、dp[1][3]=429。第 2 个 e 对 e 相同,抄 dp[1][0]=115;e 对 a 取 min(414,212)=212,e 对 t 取 min(530,328)=328。第 3 个 a 对 e 得 212,a 对 a 相同抄 dp[2][1]=115,a 对 t 取 min(425,231)=231,右下角 dp[3][3]=231。
空串边界怎么铺,无公共字符时代价是多少
整张表 (m+1)(n+1) 格、每格 O(1) 转移,时间 O(m×n);dp[i][j] 只依赖正上、正左、左上角,滚动数组只留一行把空间压到 O(n)。两个边界:某串为空则另一串全删,代价是它的 ASCII 总和;两串毫无公共字符则各自删空,答案是两串 ASCII 总和相加。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 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++ 跟着练。
面试官可能追问
这题和「两个字符串的删除操作」LeetCode 583 是什么关系?+
583 求让两串相等最少删几个字符,本题求删掉字符的 ASCII 之和最小。骨架相同——都是 dp[i][j] 记让两前缀相等的最小代价,字符相同抄左上角、不同取两个删除方向的较小。区别只在代价单位:583 每删一个字符记 1,本题每删一个记它的 ASCII 值。所以 583 里不同是 min(dp[i-1][j], dp[i][j-1])+1,本题把那个 +1 换成被删字符的 ASCII,其余一模一样。
为什么不能先求最长公共子序列、留下它、把其余删掉?+
因为最小删除代价等价于「最大化保留的公共子序列的 ASCII 权重和」,不是最大化它的长度。设两串 ASCII 总和为 S,保留的公共子序列在两串里各留一份,删除代价 = S 减去 2 倍保留子序列的 ASCII 和。要让代价最小就得让保留子序列的 ASCII 和最大,而最长的公共子序列不一定 ASCII 和最大——保留一个大 ASCII 字符可能胜过保留两个小的。所以只能做带权版,直接对删除代价 DP 最省事。
空间为什么能从 O(mn) 压到 O(n)?+
dp[i][j] 只用到正上方 dp[i-1][j]、正左方 dp[i][j-1] 和左上角 dp[i-1][j-1],全在上一行和本行已填的部分里,更早的行再也用不到。所以不必存整张表,只留一行滚动更新即可,空间从 O(m×n) 降到 O(n)。唯一要小心的是左上角 dp[i-1][j-1]:它在本行更新到当前格时会被同列的新值覆盖,得先用一个变量把旧值暂存下来,算完本格再更新,否则本格会用错左上角。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 两个字符串的最小 ASCII 删除和 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。