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]的更新依赖于三个值:dp[i-1][j-1](左上角,对角线):表示同时选取了前一个位置的两个元素。dp[i-1][j](上方):表示不选nums1的当前元素。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]更新为以下三者的最大值:(pre > 0 ? pre : 0) + num * nums2[j]:使用当前对角线值(pre)加上当前乘积。f[j+1](旧值):相当于二维中的dp[i-1][j]。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) 空间,这是一个显著的优化。
- 数组空间 :我们不再维护一个 N × M N \times M N×M 的二维数组,而是只维护一个长度为 M + 1 M + 1 M+1 的一维数组
- 结论 : O ( M ) O(M) O(M),其中 M M M 是
nums2的长度。- 优化提示 :如果在实际工程中 N < M N < M N<M,可以在代码开始前交换
nums1和nums2,使得空间复杂度变为 O ( min ( N , M ) ) O(\min(N, M)) O(min(N,M))。
- 优化提示 :如果在实际工程中 N < M N < M N<M,可以在代码开始前交换
参考灵神