LeetCode算法日记 - Day 106: 两个字符串的最小ASCII删除和

目录

[1. 两个字符串的最小ASCII删除和](#1. 两个字符串的最小ASCII删除和)

[1.1 题目解析](#1.1 题目解析)

[1.2 解法](#1.2 解法)

[1.3 代码实现](#1.3 代码实现)

1. 两个字符串的最小ASCII删除和

https://leetcode.cn/problems/minimum-ascii-delete-sum-for-two-strings/description/

给定两个字符串s1s2,返回 使两个字符串相等所需删除字符的 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 <= s1.length, s2.length <= 1000
  • s1s2 由小写英文字母组成

1.1 题目解析

题目本质

把两个字符串通过"删字符"变成一样的,删掉的字符 ASCII 值之和最小。

等价于:

  • 要么直接从"删哪些字符"角度做 DP。

  • 要么从"尽量保留哪些公共子序列(ASCII 和最大)"角度做 DP,然后用总和减去保留的部分。

常规解法

从左往右同时扫 s1、s2,在每一对位置 (i, j):

  • 如果 s1[i] == s2[j]:这两个字符都保留,继续看 (i+1, j+1)。

  • 否则有两种选择:

    • 删掉 s1[i],代价是 ASCII(s1[i]) + f(i+1, j)

    • 删掉 s2[j],代价是 ASCII(s2[j]) + f(i, j+1)

      取较小的即可

java 复制代码
class SolutionSlow {
    public int minimumDeleteSum(String s1, String s2) {
        return dfs(s1, s2, 0, 0);
    }

    // 返回使 s1[i..] 和 s2[j..] 相等的最小删除 ASCII 和
    private int dfs(String s1, String s2, int i, int j) {
        if (i == s1.length()) { // s1 走完,只能把 s2 剩下的都删掉
            int sum = 0;
            for (int k = j; k < s2.length(); k++) {
                sum += s2.charAt(k);
            }
            return sum;
        }
        if (j == s2.length()) { // s2 走完,只能把 s1 剩下的都删掉
            int sum = 0;
            for (int k = i; k < s1.length(); k++) {
                sum += s1.charAt(k);
            }
            return sum;
        }

        if (s1.charAt(i) == s2.charAt(j)) {
            // 字符相同,保留这两个,继续往后
            return dfs(s1, s2, i + 1, j + 1);
        } else {
            // 只能删其中一个,取代价较小的方案
            int deleteFromS1 = s1.charAt(i) + dfs(s1, s2, i + 1, j);
            int deleteFromS2 = s2.charAt(j) + dfs(s1, s2, i, j + 1);
            return Math.min(deleteFromS1, deleteFromS2);
        }
    }
}

问题分析

这套递归,几乎每个 (i, j) 都要反复算很多次:

  • 状态数大概是 (n+1) * (m+1)。

  • 但因为没有记忆化,很多状态会被重复调用,分支数接近 2^(n+m) 级别。

    时间:接近 O(2^(n+m)),n, m 最多到 1000,必然 TLE。

思路转折

要想高效,必须做到

  • 每个 (i, j) 状态只计算一次。

  • 要么用记忆化 DFS,要么直接改成 DP 表。

换个角度:先求出两个串的"公共子序列 ASCII 和最大值" best,总 ASCII 和记为 sum,最小删除和 = sum - 2 * best。

1.2 解法

算法思想

  1. 先把两个字符串的所有字符 ASCII 值加起来得到 sum。

  2. 用 DP 求 dp[i][j]:

  • 定义:dp[i][j] = s1[0..i-1] 和 s2[0..j-1] 的公共子序列能取得的最大 ASCII 和。

  • 转移:

    • 不用 s1[i-1]:可以从 dp[i-1][j] 获取。

    • 不用 s2[j-1]:可以从 dp[i][j-1] 获取。

    • 如果 s1[i-1] == s2[j-1]:可以使用这个字符,

      dp[i][j] = max(dp[i][j], dp[i-1][j-1] + ASCII(s1[i-1]))。

步骤拆解:

**i)**把 _s1、_s2 转成 char[],方便按下标访问。

**ii)**创建 dp[n+1][m+1],初始为 0(空串公共子序列贡献为 0)。

iii)双重循环填表:

  • 外层 i = 1..n,内层 j = 1..m。

  • 先写:dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);

  • 如果 s1[i-1] == s2[j-1],再用:

    dp[i][j] = Math.max(dp[i][j], dp[i-1][j-1] + s1[i-1]);

**iv)**再扫一遍 s1 和 s2,求 sum。

**v)**返回 sum - 2 * dp[n][m]

易错点:

  • **下标偏移:**dp 维度是 n+1, m+1,表示"前 i 个"、"前 j 个",对应字符要用 s1[i-1]、s2[j-1],不能写成 s1[i]。

  • 转移过程中的 dp[i][j] 初值:必须先用dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);确保"跳过某个字符"的两种情况都考虑到了。

  • **公式少乘了 2:**最后一定要 sum - 2 * dp[n][m],因为公共子序列里每个字符在两个字符串里各保留一份,共保留了两次。

  • **字符转 ASCII:**直接用 sum += s1[i]; 就行,char 会自动提升成 int,不用手动强转。

1.3 代码实现

java 复制代码
class Solution {
    public int minimumDeleteSum(String _s1, String _s2) {
        char[] s1 = _s1.toCharArray();
        char[] s2 = _s2.toCharArray();
        int n = s1.length;
        int m = s2.length;

        // dp[i][j] 表示 s1[0..i-1] 和 s2[0..j-1] 的公共子序列的最大 ASCII 和
        int[][] dp = new int[n + 1][m + 1];

        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                // 先考虑跳过 s1[i-1] 或跳过 s2[j-1]
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                // 如果当前字符相等,可以把这个字符加入公共子序列
                if (s1[i - 1] == s2[j - 1]) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + s1[i - 1]);
                }
            }
        }

        // 计算两个字符串所有字符的 ASCII 总和
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += s1[i];
        }
        for (int i = 0; i < m; i++) {
            sum += s2[i];
        }

        // 答案 = 总和 - 2 * 最大公共子序列 ASCII 和
        return sum - 2 * dp[n][m];
    }
}

复杂度分析

  • **时间复杂度:O(n * m),**dp 表大小为 (n+1) * (m+1),每个格子 O(1) 转移。

  • **空间复杂度:O(n * m),**一个 dp 表占 O(n * m)。

相关推荐
5***8464几秒前
Spring Boot的项目结构
java·spring boot·后端
SimonKing1 分钟前
基于Netty的TCP协议的Socket客户端
java·后端·程序员
程序员飞哥1 分钟前
几年没面试,这次真的被打醒了!
java·面试
Learner12 分钟前
Python异常处理
java·前端·python
AI科技星14 分钟前
光速飞行器动力学方程的第一性原理推导、验证与范式革命
数据结构·人工智能·线性代数·算法·机器学习·概率论
tao35566715 分钟前
VS Code登录codex,报错(os error 10013)
java·服务器·前端
橘颂TA16 分钟前
【剑斩OFFER】算法的暴力美学——leetCode 946 题:验证栈序列
c++·算法·leetcode·职场和发展·结构与算法
闻缺陷则喜何志丹18 分钟前
【状态机动态规划】3686. 稳定子序列的数量|1969
c++·算法·动态规划·力扣·状态机动态规划
信创天地22 分钟前
核心系统去 “O” 攻坚:信创数据库迁移的双轨运行与数据一致性保障方案
java·大数据·数据库·金融·架构·政务