持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第10天,点击查看活动详情
给定两个字符串s1 和 s2,返回 使两个字符串相等所需删除字符的 ASCII 值的最小和 。
示例 1:
输入: s1 = "sea", s2 = "eat"
输出: 231
解释: 在 "sea" 中删除 "s" 并将 "s" 的值(115)加入总和。
在 "eat" 中删除 "t" 并将 116 加入总和。
结束时,两个字符串相等,115 + 116 = 231 就是符合条件的最小和。
示例 2:
输入:s1 = "delete", s2 = "leet"
输出: 403
解释: 在 "delete" 中删除 "dee" 字符串变成 "let",
将 100[d]+101[e]+101[e] 加入总和。在 "leet" 中删除 "e" 将 101[e] 加入总和。
结束时,两个字符串都等于 "let",结果即为 100+101+101+101 = 403 。
如果改为将两个字符串转换为 "lee" 或 "eet",我们会得到 433 或 417 的结果,比答案更大。
动态规划
我们用 表示字符串 和 ( 表示字符串 从第 位到末尾的子串, 表示字符串 从第 位到末尾的子串,字符串下标从 0 开始)达到相等所需删除的字符的 ASCII 值的最小和,最终的答案为 。
当 s1[i:] 和 s2[j:] 中的某一个字符串为空时,dp[i][j] 的值即为另一个非空字符串的所有字符的 ASCII 值之和。例如当 s2[j:] 为空时,此时有 j = s2.length(),状态转移方程为
dp[i][j] = s1.asciiSumFromPos(i)
也可以写成递推的形式,即
dp[i][j] = dp[i + 1][j] + s1.asciiAtPos(i)
对于其余的情况,即两个字符串都非空时,如果有 s1[i] == s2[j],那么当前位置的两个字符相同,它们不需要被删除,状态转移方程为
dp[i][j] = dp[i + 1][j + 1]
如果 s1[i] != s2[j],那么我们至少要删除 s1[i] 和 s2[j] 两个字符中的一个,因此状态转移方程为
dp[i][j] = min(dp[i + 1][j] + s1.asciiAtPos(i), dp[i][j + 1] + s2.asciiAtPos(j))
/**
* @param {string} s1
* @param {string} s2
* @return {number}
*/
var minimumDeleteSum = function(s1, s2) {
let m = s1.length,
n = s2.length;
// 备忘录值为 -1 代表未曾计算
let memo = new Array(m).fill(-1).map(() => new Array(n).fill(-1));
const dp = (s1, i, s2, j) => {
/**
* 定义:将 s1[i..] 和 s2[j..] 删除成相同字符串,
* 最小的 ASCII 码之和为 dp(s1, i, s2, j)。
*/
let res = 0;
// base case
if (i == s1.length) {
// 如果 s1 到头了,那么 s2 剩下的都得删除
for (; j < s2.length; j++) {
res += s2.charCodeAt(j);
}
return res;
}
if (j == s2.length) {
// 如果 s2 到头了,那么 s1 剩下的都得删除
for (; i < s1.length; i++) {
res += s1.charCodeAt(i);
}
return res;
}
if (memo[i][j] != -1) return memo[i][j];
if (s1.charCodeAt(i) == s2.charCodeAt(j)) {
// s1[i] 和 s2[j] 都是在 lcs 中的,不用删除
memo[i][j] = dp(s1, i + 1, s2, j + 1);
} else {
// s1[i] 和 s2[j] 至少有一个不在 lcs 中,删一个
memo[i][j] = Math.min(
s1.charCodeAt(i) + dp(s1, i + 1, s2, j),
s2.charCodeAt(j) + dp(s1, i, s2, j + 1)
);
}
return memo[i][j];
};
return dp(s1, 0, s2, 0);
};