Problem: 712. 两个字符串的最小ASCII删除和
文章目录
- 整体思路
-
-
- [1. 核心问题与转换](#1. 核心问题与转换)
- [2. 算法与逻辑步骤](#2. 算法与逻辑步骤)
-
- 完整代码
- 时空复杂度
-
-
- [1. 时间复杂度: O ( N × M ) O(N \times M) O(N×M)](#1. 时间复杂度: O ( N × M ) O(N \times M) O(N×M))
- [2. 空间复杂度: O ( N × M ) O(N \times M) O(N×M)](#2. 空间复杂度: O ( N × M ) O(N \times M) O(N×M))
-
整体思路
1. 核心问题与转换
这段代码的核心目标是求出使两个字符串 s1 和 s2 相等所需删除字符的最小 ASCII 值之和。
代码并没有直接去计算"删除"了什么,而是采用了一个巧妙的逆向思维 转换:
删除的最小 ASCII 和 = ( s1 和 s2 的总 ASCII 和 ) − ( s1 和 s2 最长公共子序列的 ASCII 和 ) \text{删除的最小 ASCII 和} = (\text{s1 和 s2 的总 ASCII 和}) - (\text{s1 和 s2 最长公共子序列的 ASCII 和}) 删除的最小 ASCII 和=(s1 和 s2 的总 ASCII 和)−(s1 和 s2 最长公共子序列的 ASCII 和)
- 如果两个字符被保留(即它们属于公共子序列),那么它们不需要被删除。
- 因为最终保留的字符串在
s1和s2中是相同的,所以对于公共子序列中的每一个字符c,它在s1中贡献了ASCII(c),在s2中也贡献了ASCII(c)。因此,公共部分的权重是2 * ASCII(c)。
2. 算法与逻辑步骤
该解法使用了记忆化搜索(DFS + Memoization),即自顶向下的动态规划。
-
第一步:计算总和
- 计算
s1和s2所有字符的 ASCII 码总和sum。
- 计算
-
第二步:DFS 计算最长公共子序列(LCS)的最大权重
dfs(i, j)定义为:计算s1[0...i]和s2[0...j]之间公共子序列的最大 ASCII 和(注意:这里计算的是双倍和,即算上了两边的贡献)。- 状态转移 :
- 匹配 :如果
s1[i] == s2[j],说明这个字符可以作为公共子序列的一部分。递归结果为dfs(i-1, j-1) + s1[i] * 2。 - 不匹配 :如果字符不同,则必须舍弃
s1[i]或者s2[j]中的一个。我们取两者中能产生更大公共和的路径:Math.max(dfs(i-1, j), dfs(i, j-1))。
- 匹配 :如果
- 边界条件 :当
i < 0或j < 0,说明有一个字符串遍历完了,公共子序列和为 0。
-
第三步:得出结果
- 结果 =
TotalSum - MaxCommonSum。
- 结果 =
完整代码
java
import java.util.Arrays;
class Solution {
// 将字符串转换为字符数组,方便通过索引访问
private char[] s, t;
// 记忆化数组,memo[i][j] 存储 s1[0...i] 和 s2[0...j] 的最大公共 ASCII 和
private int[][] memo;
// 深度优先搜索函数
// i: s1 当前处理到的尾部索引
// j: s2 当前处理到的尾部索引
private int dfs(int i, int j) {
// 递归终止条件(Base Case):
// 如果任意一个字符串遍历结束(索引小于 0),则公共部分为 0
if (i < 0 || j < 0) {
return 0;
}
// 记忆化检查:
// 如果当前状态 (i, j) 已经计算过(不等于初始值 -1),直接返回缓存的结果
if (memo[i][j] != -1) {
return memo[i][j];
}
// 核心状态转移逻辑:
// 情况 1:当前两个字符相等,可以成为公共子序列的一部分
if (s[i] == t[j]) {
// 递归去处理前面的部分 (i-1, j-1)
// 加上 s[i] * 2,因为这个字符在 s1 和 s2 中都保留了,
// 而我们在初始计算 total sum 时是把 s1 和 s2 的所有字符都加起来的。
// 要算出"删除的剩余部分",这里需要扣除两边的贡献。
memo[i][j] = dfs(i - 1, j - 1) + s[i] * 2;
}
// 情况 2:当前两个字符不相等
else {
// 只能舍弃 s[i] 或者 t[j] 中的一个,看哪种情况能保留更大的公共 ASCII 和
// dfs(i - 1, j): 尝试跳过 s[i]
// dfs(i, j - 1): 尝试跳过 t[j]
memo[i][j] = Math.max(dfs(i - 1, j), dfs(i, j - 1));
}
// 返回计算结果
return memo[i][j];
}
public int minimumDeleteSum(String s1, String s2) {
// 1. 计算两个字符串所有字符的 ASCII 码总和
// 使用 Java 8 Stream API 快速求和
int sum = s1.chars().sum() + s2.chars().sum();
// 2. 初始化数据结构
s = s1.toCharArray();
t = s2.toCharArray();
int n = s.length;
int m = t.length;
// 初始化记忆化数组,大小为 n x m
memo = new int[n][m];
// 填充 -1 表示状态尚未被计算
for (int[] row : memo) {
Arrays.fill(row, -1);
}
// 3. 计算结果
// 最小删除和 = 总和 - (两边都保留的公共子序列的最大 ASCII 和)
// dfs(n - 1, m - 1) 返回的是 s1 和 s2 最大公共 ASCII 权重的两倍
return sum - dfs(n - 1, m - 1);
}
}
时空复杂度
1. 时间复杂度: O ( N × M ) O(N \times M) O(N×M)
- 计算依据 :
- 这是一个典型的二维动态规划问题。
- 状态的数量由输入字符串的长度决定,共有 N × M N \times M N×M 个不同的状态
(i, j),其中 N N N 是s1的长度, M M M 是s2的长度。 - 由于使用了记忆化数组
memo,每个状态最多只会被实际计算一次。 - 每次计算内部只包含常数级别的比较和加法操作 O ( 1 ) O(1) O(1)。
- 结论 : O ( N × M ) O(N \times M) O(N×M)。
2. 空间复杂度: O ( N × M ) O(N \times M) O(N×M)
- 计算依据 :
- 记忆化数组 :需要一个大小为 N × M N \times M N×M 的二维整数数组
memo来存储中间结果。 - 递归栈空间 :递归的最大深度为 N + M N + M N+M(最坏情况下,每次只减少一个索引)。但在 Big O 表示法中,通常数组的空间占主导地位。
- 字符数组
s和t占用 O ( N + M ) O(N + M) O(N+M) 空间。
- 记忆化数组 :需要一个大小为 N × M N \times M N×M 的二维整数数组
- 结论 : O ( N × M ) O(N \times M) O(N×M)。