LeetCode 712. 两个字符串的最小ASCII删除和
题目描述

题意分析
可以从两个字符串中删除字符,保留字符的相对顺序不变,最终使两串内容完全相同。每删除一个字符,就支付它的 ASCII 值,要求总费用最小。
不同字符的费用不同,因此只求最少删除几个字符或最长公共子序列的长度还不够;需要在选择保留、删除时直接比较费用。题目只包含小写英文字母,代码读取的字符值就等于所需的 ASCII 值。
解法:动态规划状态转移
核心思路
[!blue]
设两串长度为
m、n。定义dp[i][j]为将后缀s1[i..m-1]与s2[j..n-1]删除成相同字符串的最小费用。下标表示还没处理的第一个字符,所以最终答案是dp[0][0]。先确定空后缀的边界:两侧都为空时费用是 $0$;只有一侧为空时,另一侧没有字符可以保留配对,只能全部删除。因此
dp[i][n]是s1从i起的字符值总和,dp[m][j]是s2从j起的字符值总和。两侧都有字符时,比较当前首字符:
- 相同:可以保留这两个字符并让它们配对,不付费用,问题缩小为
dp[i + 1][j + 1]。这样不会损失最优方案:若一个方案把这两个首字符都删了,可以把它们一起保留在公共串开头;若用首字符与另一串后面的同字符配对,也可以把这次配对移到当前两个首字符,后续配对顺序不受影响,费用不会增加。- 不同:两者不能同时成为剩余字符串的首字符,至少要删除其中一个。删除
s1[i]的费用是s1[i] + dp[i + 1][j];删除s2[j]的费用是s2[j] + dp[i][j + 1],取较小者。两者都删的方案已经包含在先删一侧、再由后续状态删另一侧的过程中,无需额外列一种转移。每个状态依赖下方、右方或右下方的状态,所以先填最后一行、最后一列,再让
i、j都从大到小遍历。这样计算当前格时,所需子问题已经完成;所有状态填完后,dp[0][0]就包含两串全部字符的最优选择。
解题步骤
- 创建
(m + 1) × (n + 1)的状态表,dp[m][n]保持为 $0$。- 倒序累加各自后缀的字符值,填好
dp[i][n]与dp[m][j]。- 两维倒序遍历:当前字符相同就继承右下方;不同就比较删除左串或右串当前字符的总费用。
- 返回
dp[0][0]。
代码实现
class Solution {
// 若字符相等则继承 dp[i+1][j+1],否则取删除一边的最小代价。
public int minimumDeleteSum(String s1, String s2) {
int m = s1.length();
int n = s2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = m - 1; i >= 0; i--) {
// 另一侧为空,当前后缀只能全部删除
dp[i][n] = dp[i + 1][n] + s1.charAt(i);
}
for (int j = n - 1; j >= 0; j--) {
dp[m][j] = dp[m][j + 1] + s2.charAt(j);
}
for (int i = m - 1; i >= 0; i--) {
for (int j = n - 1; j >= 0; j--) {
if (s1.charAt(i) == s2.charAt(j)) {
// 相同首字符保留配对,不增加删除费用
dp[i][j] = dp[i + 1][j + 1];
} else {
int del1 = s1.charAt(i) + dp[i + 1][j];
int del2 = s2.charAt(j) + dp[i][j + 1];
dp[i][j] = Math.min(del1, del2);
}
}
}
return dp[0][0];
}
}
func minimumDeleteSum(s1 string, s2 string) int {
// 若字符相等则继承 dp[i+1][j+1],否则取删除一边的最小代价。
m := len(s1)
n := len(s2)
dp := make([][]int, m+1)
for i := 0; i <= m; i++ {
dp[i] = make([]int, n+1)
}
for i := m - 1; i >= 0; i-- {
// 另一侧为空,当前后缀只能全部删除
dp[i][n] = dp[i+1][n] + int(s1[i])
}
for j := n - 1; j >= 0; j-- {
dp[m][j] = dp[m][j+1] + int(s2[j])
}
for i := m - 1; i >= 0; i-- {
for j := n - 1; j >= 0; j-- {
if s1[i] == s2[j] {
// 相同首字符保留配对,不增加删除费用
dp[i][j] = dp[i+1][j+1]
} else {
del1 := int(s1[i]) + dp[i+1][j]
del2 := int(s2[j]) + dp[i][j+1]
if del1 < del2 {
dp[i][j] = del1
} else {
dp[i][j] = del2
}
}
}
}
return dp[0][0]
}
复杂度分析
- 时间复杂度:$O((m+1)(n+1))$,包含空后缀边界的初始化。
- 空间复杂度:$O((m+1)(n+1))$,完整后缀状态表。
关键点总结
[!green]
- 若保留的公共子序列权值为
W,删除费用就是两串字符值总和减去2W,所以应优化权值而非长度。- 两侧都删除的选择已包含在连续单侧删除中。
易错点总结
[!yellow]
- 边界全部留零,会漏掉另一串仍需删除的字符代价。
- 使用字母序号而非 ASCII 值,会改变费用。
- 按正序计算,会读取尚未完成的后缀状态。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 583. 两个字符串的删除操作 | 中等 | 原题每次删除成本为1,本题成本为字符ASCII值,需要把长度目标改成加权保留或删除代价。 |
| 1143. 最长公共子序列 | 中等 | 可转成最大公共子序列的ASCII权重和,再由两串总权重减去两倍保留权重。 |
| 72. 编辑距离 | 中等 | 用两个前缀构成二维动态规划状态;本题删除代价改为字符编码之和,该题允许插入删除替换并求最小代价。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!