【LeetCode 每日一题】712. 两个字符串的最小ASCII删除和——(解法三)状态压缩

Problem: 712. 两个字符串的最小ASCII删除和

文章目录

  • 整体思路
      • [1. 核心问题与转换](#1. 核心问题与转换)
      • [2. 算法优化:状态压缩(1D DP)](#2. 算法优化:状态压缩(1D DP))
  • 完整代码
  • 时空复杂度
      • [1. 时间复杂度: O ( N × M ) O(N \times M) O(N×M)](#1. 时间复杂度: O ( N × M ) O(N \times M) O(N×M))
      • [2. 空间复杂度: O ( M ) O(M) O(M)](#2. 空间复杂度: O ( M ) O(M) O(M))

整体思路

1. 核心问题与转换

这段代码依然沿用了 "总和 - 最大公共子序列(LCS)权重" 的逆向思维策略。

核心公式保持不变:
最小删除和 = ( s1总和 + s2总和 ) − ( LCS字符ASCII和 × 2 ) \text{最小删除和} = (\text{s1总和} + \text{s2总和}) - (\text{LCS字符ASCII和} \times 2) 最小删除和=(s1总和+s2总和)−(LCS字符ASCII和×2)

2. 算法优化:状态压缩(1D DP)

与上一版二维数组解法不同,这里使用了一维数组进行空间优化。

  • 空间压缩原理

    在二维 DP 中,计算 dp[i][j] 只需要用到上一行的数据 dp[i-1][...] 和当前行左边的数据 dp[i][j-1]

    • f[j+1] 在更新前,存储的是上一行对应位置的值(相当于 dp[i-1][j])。
    • f[j] 在更新后,存储的是当前行左边位置的值(相当于 dp[i][j-1])。
    • 难点 :在于如何获取 dp[i-1][j-1](左上角的值)。因为在更新 f[j+1] 之前,f[j] 已经被更新为当前行的值了。
    • 解决方案 :引入 pre 变量,专门用来暂存上一行对角线位置的值。
  • 逻辑流程

    1. 初始化长度为 m + 1 的数组 f,初始全为 0(代表空串时的公共和)。
    2. 外层循环遍历 s1 的字符 x
    3. 在内层循环开始前,初始化 pre = 0(代表第 0 列的左上角,即空前缀)。
    4. 内层循环遍历 s2 的索引 j
      • 先用 temp 保存 f[j+1] 的旧值(即下一轮需要的左上角值)。
      • 根据 xt[j] 是否相等,利用 pref[j+1](旧)、f[j](新)更新 f[j+1]
      • 更新 pre = temp,为下一个位置做准备。

完整代码

java 复制代码
class Solution {
    public int minimumDeleteSum(String s1, String s2) {
        // 1. 计算两字符串初始的总 ASCII 和
        int sum = s1.chars().sum() + s2.chars().sum();
        
        char[] s = s1.toCharArray();
        char[] t = s2.toCharArray();
        int m = t.length;
        
        // 2. 创建一维 DP 数组
        // f[k] 表示 s1 当前处理到的前缀与 s2 的前 k 个字符的最大公共 ASCII 权重和
        int[] f = new int[m + 1];
        // Java 中 int 数组默认初始化为 0,符合逻辑(空串的公共和为 0)

        // 外层循环:遍历 s1 的每个字符 x
        for (char x : s) {
            // pre 用于维护 "左上角" (diagonal) 的状态
            // 相当于二维 DP 中的 dp[i-1][j-1]
            // 对于每一行的第一个元素,其左上角是 f[0],始终为 0
            int pre = 0;
            
            // 内层循环:遍历 s2 的每个位置 j
            for (int j = 0; j < m; j++) {
                // temp 暂存 f[j+1] 在更新前的值
                // 这个值对应二维 DP 中的 dp[i-1][j] (上一行的值)
                // 在下一次循环 (j+1) 时,它将变成 "左上角" (pre)
                int temp = f[j + 1];
                
                // 状态转移逻辑
                if (x == t[j]) {
                    // 字符匹配:
                    // 当前值 = 左上角值 (pre) + 当前字符 ASCII * 2
                    f[j + 1] = pre + x * 2;
                } else {
                    // 字符不匹配:
                    // 取 "上方" (f[j+1] 旧值) 和 "左方" (f[j] 新值) 的最大值
                    f[j + 1] = Math.max(f[j + 1], f[j]);
                }
                
                // 更新 pre,将当前的旧值传递给下一次内层循环作为左上角使用
                pre = temp;
            }
        }
        
        // 3. 计算结果
        // 总和 - 最大保留部分的和
        return sum - f[m];
    }
}

时空复杂度

1. 时间复杂度: O ( N × M ) O(N \times M) O(N×M)

  • 计算依据
    • 双重循环结构没有改变。
    • 外层循环执行 N N N 次(s1 的长度),内层循环执行 M M M 次(s2 的长度)。
    • 内部操作均为 O ( 1 ) O(1) O(1)。
  • 结论 : O ( N × M ) O(N \times M) O(N×M)。

2. 空间复杂度: O ( M ) O(M) O(M)

  • 计算依据
    • 这是此代码的主要亮点。
    • 我们将原本 ( N + 1 ) × ( M + 1 ) (N+1) \times (M+1) (N+1)×(M+1) 的二维数组压缩为了长度为 M + 1 M+1 M+1 的一维数组 f
    • 额外使用了几个常数变量(pre, temp 等)。
    • 空间消耗仅与 s2 的长度线性相关。
  • 结论 : O ( M ) O(M) O(M),其中 M M M 是 s2 的长度。
    • 优化提示 :如果 N < M N < M N<M,可以在代码开始前交换 s1s2,使空间复杂度进一步优化为 O ( min ⁡ ( N , M ) ) O(\min(N, M)) O(min(N,M))。
相关推荐
生锈的键盘1 小时前
推荐算法实践:交叉特征的理解
算法
乌萨奇也要立志学C++2 小时前
【洛谷】BFS 求解最短路:从马的遍历到迷宫问题的实战解析
算法·宽度优先
老鼠只爱大米2 小时前
LeetCode经典算法面试题 #46:全排列(回溯、交换、剪枝等五种实现方案详细解析)
算法·leetcode·剪枝·回溯·全排列·stj算法
Dovis(誓平步青云)2 小时前
《滑动窗口算法:从 “暴力遍历” 到 “线性高效” 的思维跃迁》
运维·服务器·数据库·算法
_OP_CHEN2 小时前
【算法基础篇】(五十七)线性代数之矩阵乘法从入门到实战:手撕模板 + 真题详解
线性代数·算法·矩阵·蓝桥杯·c/c++·矩阵乘法·acm/icpc
天天爱吃肉82182 小时前
【跨界封神|周杰伦×王传福(陶晶莹主持):音乐创作与新能源NVH测试,底层逻辑竟完全同源!(新人必看入行指南)】
python·嵌入式硬件·算法·汽车
im_AMBER2 小时前
Leetcode 114 链表中的下一个更大节点 | 删除排序链表中的重复元素 II
算法·leetcode
xhbaitxl3 小时前
算法学习day38-动态规划
学习·算法·动态规划
多恩Stone3 小时前
【3D AICG 系列-6】OmniPart 训练流程梳理
人工智能·pytorch·算法·3d·aigc
历程里程碑3 小时前
普通数组----轮转数组
java·数据结构·c++·算法·spring·leetcode·eclipse