【LeetCode 每日一题】1458. 两个子序列的最大点积——(解法三)状态压缩

Problem: 1458. 两个子序列的最大点积

整体思路

1. 核心问题与功能

这段代码的功能与之前的版本完全一致:解决 LeetCode 1458. 两个子序列的最大点积 问题。

它的核心目标是在保持 O ( N × M ) O(N \times M) O(N×M) 时间复杂度的同时,将空间复杂度从 O ( N × M ) O(N \times M) O(N×M) 优化到 O ( M ) O(M) O(M)。

2. 算法与逻辑步骤

该解法使用了一维数组进行动态规划(状态压缩)

  • 数据结构选择

    • 仅使用一个长度为 m + 1 的一维数组 f
    • 在标准的二维 DP 中,dp[i][j] 的更新依赖于三个值:
      1. dp[i-1][j-1](左上角,对角线):表示同时选取了前一个位置的两个元素。
      2. dp[i-1][j](上方):表示不选 nums1 的当前元素。
      3. dp[i][j-1](左方):表示不选 nums2 的当前元素。
    • 在一维数组中,f[j+1] 在更新前存储的是上一轮循环(即 i-1 层)的值,相当于 dp[i-1][j]。更新后存储的是当前轮(i 层)的值。
  • 核心逻辑与变量变换

    • pre 变量 :这是空间优化的关键。由于我们是一行行更新 f 数组的,当我们更新 f[j+1] 时,f[j] 已经是当前行的新值了,而我们需要的是上一行f[j](也就是 dp[i-1][j-1])。因此,我们使用变量 pre 来临时保存被覆盖前的旧值,充当"左上角"的状态。
    • tmp 变量 :在覆盖 f[j+1] 之前,先将其值存入 tmp。这个值在下一次内层循环(j 增加 1)时,将成为新的 pre(即下一列的左上角)。
  • 状态转移

    • nums1 的元素由外层循环控制(num)。
    • nums2 的元素由内层循环控制(nums2[j])。
    • f[j+1] 更新为以下三者的最大值:
      1. (pre > 0 ? pre : 0) + num * nums2[j]:使用当前对角线值(pre)加上当前乘积。
      2. f[j+1](旧值):相当于二维中的 dp[i-1][j]
      3. f[j](新值):相当于二维中的 dp[i][j-1]

完整代码

java 复制代码
import java.util.Arrays;

class Solution {
    public int maxDotProduct(int[] nums1, int[] nums2) {
        int m = nums2.length;
        
        // 创建一维 DP 数组,长度为 nums2 的长度 + 1
        // 这一维数组相当于二维 DP 表中的 "上一行"
        int[] f = new int[m + 1];
        
        // 初始化为最小值,因为点积可能为负数
        Arrays.fill(f, Integer.MIN_VALUE);

        // 外层循环遍历 nums1 的每一个元素
        // 对应二维 DP 中的行索引 i
        for (int num : nums1) {
            
            // pre 用于维护 "左上角" (diagonal) 的状态,即 dp[i-1][j-1]
            // 在每一行的开始,左上角边界通常视为 MIN_VALUE (或 0 的变体,视具体逻辑)
            // 这里 f[0] 始终保持 MIN_VALUE (边界),所以初始 pre 是 MIN_VALUE
            int pre = f[0];
            
            // 内层循环遍历 nums2 的每一个元素
            // 对应二维 DP 中的列索引 j
            for (int j = 0; j < m; j++) {
                
                // tmp 暂存 f[j+1] 更新前的值。
                // f[j+1] 在更新前代表 dp[i-1][j] (上一行的值)。
                // 当 j 进入下一轮循环时,这个 tmp 就会变成那时所需的 "左上角" (pre)。
                int tmp = f[j + 1];
                
                // 计算当前两个数值的乘积
                int product = num * nums2[j];
                
                // 计算第一种情况:使用当前这两个数 num 和 nums2[j]
                // Math.max(pre, 0) 的作用:如果之前的对角线路径和 (pre) 是负数,
                // 则抛弃前面,只保留当前的 product(相当于开启新子序列)。
                // 否则延续前面的子序列。
                int pickCurrent = Math.max(pre, 0) + product;
                
                // 状态转移方程:取三者最大值
                // 1. pickCurrent: 选当前这对
                // 2. f[j+1] (旧值): 不选 num (即 nums1[i]),继承自上方状态
                // 3. f[j] (新值): 不选 nums2[j],继承自左方状态 (注意 f[j] 在本轮循环刚被更新过)
                f[j + 1] = Math.max(pickCurrent, Math.max(f[j + 1], f[j]));
                
                // 更新 pre,为下一次循环 (j+1) 做准备
                // 当前位置的 "旧值" (tmp) 将成为下一个位置的 "左上角"
                pre = tmp;
            }
        }
        
        // 返回数组最后一个元素,即考虑了所有元素后的最大点积
        return f[m];
    }
}

时空复杂度

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

  • 计算依据
    • 代码依旧包含两层嵌套循环。
    • 外层循环遍历 nums1 ( N N N 次)。
    • 内层循环遍历 nums2 ( M M M 次)。
    • 内部操作均为常数级别的加法、乘法和比较。
  • 结论 : O ( N × M ) O(N \times M) O(N×M)。

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

  • 计算依据
    • 数组空间 :我们不再维护一个 N × M N \times M N×M 的二维数组,而是只维护一个长度为 M + 1 M + 1 M+1 的一维数组 f
    • 辅助变量 :仅使用了 pre, tmp, num, product 等几个常数级额外变量。
    • 相比于前一个版本的 O ( N × M ) O(N \times M) O(N×M) 空间,这是一个显著的优化。
  • 结论 : O ( M ) O(M) O(M),其中 M M M 是 nums2 的长度。
    • 优化提示 :如果在实际工程中 N < M N < M N<M,可以在代码开始前交换 nums1nums2,使得空间复杂度变为 O ( min ⁡ ( N , M ) ) O(\min(N, M)) O(min(N,M))。

参考灵神

相关推荐
位东风2 小时前
希尔排序(Shell Sort)详解
算法·排序算法
AI科技星2 小时前
光速飞行器动力学方程的第一性原理推导、验证与范式革命
数据结构·人工智能·线性代数·算法·机器学习·概率论
橘颂TA2 小时前
【剑斩OFFER】算法的暴力美学——leetCode 946 题:验证栈序列
c++·算法·leetcode·职场和发展·结构与算法
闻缺陷则喜何志丹2 小时前
【状态机动态规划】3686. 稳定子序列的数量|1969
c++·算法·动态规划·力扣·状态机动态规划
寻星探路2 小时前
【算法通关】双指针技巧深度解析:从基础到巅峰(Java 最优解)
java·开发语言·人工智能·python·算法·ai·指针
wen__xvn2 小时前
力扣第 484 场周赛
算法·leetcode·职场和发展
YuTaoShao2 小时前
【LeetCode 每日一题】865. 具有所有最深节点的最小子树——(解法一)自顶向下
算法·leetcode·职场和发展
2301_800895103 小时前
hh的蓝桥杯每日一题--拔河
职场和发展·蓝桥杯