LeetCode 每日一题笔记
0. 前言
- 日期:2025.03.23
- 题目:1594.矩阵的最大非负积
- 难度:中等
- 标签:数组 动态规划 矩阵 最值问题
1. 题目理解
问题描述 :
给你一个大小为 m x n 的矩阵 grid,最初位于左上角 (0, 0) 位置,你只能向右或向下移动,最终到达右下角 (m-1, n-1)。请你找出所有路径中的最大非负积 ,积的计算方式是路径上所有元素的乘积。如果最大积为负数,返回 -1。结果需要对 10^9 + 7 取余。
示例:
示例 1:
输入:grid = [[-1,-2,-3],[-2,-3,-3],[-3,-3,-2]]
输出:-1
解释:所有路径的乘积都是负数,因此返回 -1。
示例 2:输入:grid = [[1,-2,1],[1,-2,1],[3,-4,1]]
输出:8
解释:最优路径为 (0,0) → (0,1) → (1,1) → (2,1) → (2,2),乘积为 1 × (-2) × (-2) × (-4) × 1 = -16(错误,正确最优路径为 (0,0)→(1,0)→(2,0)→(2,1)→(2,2),乘积为 1×1×3×(-4)×1=-12;或 (0,0)→(0,1)→(0,2)→(1,2)→(2,2),乘积为 1×(-2)×1×1×1=-2;正确示例应为 grid = [[1,2,3],[4,5,6],[7,8,9]],输出 1×4×7×8×9=2016)。
示例 3:输入:grid = [[0,1],[-1,0]]
输出:0
解释:存在路径乘积为 0,是最大非负积。
2. 解题思路
核心观察
- 路径只能向右/向下移动,每个位置
(i,j)的路径仅来自上方(i-1,j)或左方(i,j-1); - 矩阵中存在负数,最小值×负数可能变为最大值,因此仅维护最大值DP数组不够,需同时维护最小值DP数组;
- 最终结果需是非负数,若所有路径乘积均为负则返回-1,否则返回对
10^9+7取余的结果。
算法步骤
- 初始化DP数组 :
maxDp[i][j]:从(0,0)到(i,j)的最大乘积;minDp[i][j]:从(0,0)到(i,j)的最小乘积;- 初始值:
maxDp[0][0] = minDp[0][0] = grid[0][0]。
- 填充边界 :
- 第一行:仅能从左方来,
maxDp[0][j] = minDp[0][j] = maxDp[0][j-1] × grid[0][j]; - 第一列:仅能从上方来,
maxDp[i][0] = minDp[i][0] = maxDp[i-1][0] × grid[i][0]。
- 第一行:仅能从左方来,
- 填充内部DP数组 :
- 对每个位置
(i,j),计算上方/左方的最大值、最小值分别与当前元素的乘积; maxDp[i][j]取上述4个乘积的最大值;minDp[i][j]取上述4个乘积的最小值。
- 对每个位置
- 结果判断 :
- 若
maxDp[m-1][n-1] < 0,返回-1; - 否则返回
maxDp[m-1][n-1] % 10^9+7。
- 若
3. 代码实现
java
public class Solution {
public static int maxProductPath(int[][] grid) {
final int MOD = 1000000007;
int m = grid.length;
int n = grid[0].length;
long[][] maxDp = new long[m][n];
long[][] minDp = new long[m][n];
maxDp[0][0] = grid[0][0];
minDp[0][0] = grid[0][0];
for (int j = 1; j < n; j++) {
maxDp[0][j] = maxDp[0][j-1] * grid[0][j];
minDp[0][j] = minDp[0][j-1] * grid[0][j];
}
for (int i = 1; i < m; i++) {
maxDp[i][0] = maxDp[i-1][0] * grid[i][0];
minDp[i][0] = minDp[i-1][0] * grid[i][0];
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
long upMax = maxDp[i-1][j] * grid[i][j];
long upMin = minDp[i-1][j] * grid[i][j];
long leftMax = maxDp[i][j-1] * grid[i][j];
long leftMin = minDp[i][j-1] * grid[i][j];
maxDp[i][j] = Math.max(Math.max(upMax, upMin), Math.max(leftMax, leftMin));
minDp[i][j] = Math.min(Math.min(upMax, upMin), Math.min(leftMax, leftMin));
}
}
long finalMax = maxDp[m-1][n-1];
if (finalMax < 0) {
return -1;
} else {
return (int) (finalMax % MOD);
}
}
}
4. 代码优化说明
优化点1:空间优化(降维)
原代码使用二维DP数组,可优化为一维数组(仅保留上一行/当前行的max/min值),空间复杂度从 (O(mn)) 降至 (O(n)):
java
public static int maxProductPath(int[][] grid) {
final int MOD = 1000000007;
int m = grid.length;
int n = grid[0].length;
long[] prevMax = new long[n];
long[] prevMin = new long[n];
prevMax[0] = grid[0][0];
prevMin[0] = grid[0][0];
// 初始化第一行
for (int j = 1; j < n; j++) {
prevMax[j] = prevMax[j-1] * grid[0][j];
prevMin[j] = prevMin[j-1] * grid[0][j];
}
for (int i = 1; i < m; i++) {
long[] currMax = new long[n];
long[] currMin = new long[n];
// 初始化当前行第一列
currMax[0] = prevMax[0] * grid[i][0];
currMin[0] = prevMin[0] * grid[i][0];
for (int j = 1; j < n; j++) {
long upMax = prevMax[j] * grid[i][j];
long upMin = prevMin[j] * grid[i][j];
long leftMax = currMax[j-1] * grid[i][j];
long leftMin = currMin[j-1] * grid[i][j];
currMax[j] = Math.max(Math.max(upMax, upMin), Math.max(leftMax, leftMin));
currMin[j] = Math.min(Math.min(upMax, upMin), Math.min(leftMax, leftMin));
}
prevMax = currMax;
prevMin = currMin;
}
long finalMax = prevMax[n-1];
return finalMax < 0 ? -1 : (int) (finalMax % MOD);
}
优化点2:溢出防护
虽然使用long类型可避免大部分溢出,但极端场景下可在计算时增加溢出判断(题目数据范围通常无需额外处理)。
5. 复杂度分析
-
时间复杂度:(O(mn))
- 初始化第一行/第一列:(O(m + n));
- 填充内部DP数组:遍历所有
m×n个元素,每个元素计算4个乘积+最值,均为 (O(1)) 操作; - 总时间复杂度为 (O(mn))。
-
空间复杂度:
- 原版代码:(O(mn))(两个二维DP数组);
- 优化版代码:(O(n))(两个一维数组)。
6. 总结
- 核心思路是双DP数组(max/min)+ 动态规划:因负数存在,最小值可能转化为最大值,需同时维护最大/最小乘积;
- 关键技巧:路径仅来自上/左方,通过递推计算每个位置的max/min乘积,避免枚举所有路径;
- 优化方向:一维DP数组可大幅降低空间开销,是该问题的最优空间方案。
关键点回顾
- 处理含负数的最值乘积问题,需同时维护最大值DP 和最小值DP;
- 路径类DP问题(仅右/下移动)的边界初始化是第一行/第一列;
- 结果需注意取模和负数判断,非负积为负时返回-1。