leetcode
leetcode 1451 ~ 1500
矩阵的最大非负积

矩阵的最大非负积

难度:

标签:

题目描述

给你一个大小为 m x n 的矩阵 grid 。最初,你位于左上角 (0, 0) ,每一步,你可以在矩阵中 向右向下 移动。

在从左上角 (0, 0) 开始到右下角 (m - 1, n - 1) 结束的所有路径中,找出具有 最大非负积 的路径。路径的积是沿路径访问的单元格中所有整数的乘积。

返回 最大非负积 109 + 7 取余 的结果。如果最大积为 负数 ,则返回 -1

注意,取余是在得到最大积之后执行的。

 

示例 1:

输入:grid = [[-1,-2,-3],[-2,-3,-3],[-3,-3,-2]]
输出:-1
解释:从 (0, 0) 到 (2, 2) 的路径中无法得到非负积,所以返回 -1 。

示例 2:

输入:grid = [[1,-2,1],[1,-2,1],[3,-4,1]]
输出:8
解释:最大非负积对应的路径如图所示 (1 * 1 * -2 * -4 * 1 = 8)

示例 3:

输入:grid = [[1,3],[0,-4]]
输出:0
解释:最大非负积对应的路径如图所示 (1 * 0 * -4 = 0)

 

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 15
  • -4 <= grid[i][j] <= 4

代码结果

运行时间: 26 ms, 内存: 16.1 MB


/*
 * 思路:
 * 虽然使用 Java Stream 不能直接处理动态规划的问题,但我们可以通过 Stream API 来简化代码。
 * 我们依然维护两个矩阵 dpMax 和 dpMin,然后在每一步中使用 Stream 来更新它们的值。
 */
import java.util.stream.IntStream;

public class Solution {
    public int maxProductPath(int[][] grid) {
        int MOD = 1000000007;
        int m = grid.length, n = grid[0].length;
        long[][] dpMax = new long[m][n];
        long[][] dpMin = new long[m][n];

        dpMax[0][0] = dpMin[0][0] = grid[0][0];

        // 初始化第一行
        IntStream.range(1, n).forEach(j -> dpMax[0][j] = dpMin[0][j] = dpMax[0][j - 1] * grid[0][j]);

        // 初始化第一列
        IntStream.range(1, m).forEach(i -> dpMax[i][0] = dpMin[i][0] = dpMax[i - 1][0] * grid[i][0]);

        // 填充 dp 数组
        IntStream.range(1, m).forEach(i -> {
            IntStream.range(1, n).forEach(j -> {
                if (grid[i][j] >= 0) {
                    dpMax[i][j] = Math.max(dpMax[i - 1][j], dpMax[i][j - 1]) * grid[i][j];
                    dpMin[i][j] = Math.min(dpMin[i - 1][j], dpMin[i][j - 1]) * grid[i][j];
                } else {
                    dpMax[i][j] = Math.min(dpMin[i - 1][j], dpMin[i][j - 1]) * grid[i][j];
                    dpMin[i][j] = Math.max(dpMax[i - 1][j], dpMax[i][j - 1]) * grid[i][j];
                }
            });
        });

        long result = dpMax[m - 1][n - 1];
        return result < 0 ? -1 : (int)(result % MOD);
    }
}

解释

方法:

该题解采用动态规划的方法来解决问题。定义两个二维数组 max_product 和 min_product,其中 max_product[i][j] 存储到达单元格 (i, j) 的所有路径中可能的最大积,min_product[i][j] 存储可能的最小积。这是因为负数的存在,最小积乘以负数可能变成最大积。初始状态设置为 max_product[0][0] 和 min_product[0][0] 等于 grid[0][0]。然后分别初始化矩阵的第一行和第一列。对于其他单元格,根据当前单元格值的正负来决定如何更新 max_product 和 min_product。遍历完成后,max_product[m-1][n-1] 存储的就是到达右下角的最大积。如果这个最大积小于 0,直接返回 -1;否则,返回其对 10^9+7 取模的结果。

时间复杂度:

O(m*n)

空间复杂度:

O(m*n)

代码细节讲解

🦆
在动态规划中,为什么需要同时维护最大积`max_product`和最小积`min_product`两个矩阵?
在动态规划中,同时维护最大积`max_product`和最小积`min_product`两个矩阵是非常关键的,尤其是在矩阵中存在负数的情况下。由于负数乘以一个较小的负数可以得到一个较大的正数,因此最小积乘以一个负数可能会变成最大积。同时维护这两个值可以确保在每个步骤中,无论当前元素是正是负,我们都能正确地计算出到达当前单元格的最大可能积和最小可能积。
🦆
该算法如何处理矩阵中的负数值,以确保最终可能得到一个非负的最大积?
该算法通过在每个单元格更新`max_product`和`min_product`时特别注意元素的符号来处理负数。如果当前元素是正数,我们假设最大积是由之前的最大积计算得到的,而最小积是由之前的最小积计算得到的。如果元素是负数,我们将之前的最小积(可能是一个负值)和当前元素相乘,这可能会得到一个大的正数,因此更新最大积;同样,最大积(如果是正数)乘以负数将会得到一个更小的数,所以用它来更新最小积。这种处理确保了不会错过由于负数乘积反转而产生的最大正积的可能性。
🦆
在初始化第一行和第一列时,如果遇到0或负数,这对`max_product`和`min_product`的更新有什么特别影响?
在初始化第一行和第一列时,如果遇到0,那么从这一点开始到达任何后续点的积将会是0,因为任何数与0的乘积都是0。这意味着`max_product`和`min_product`在该位置及之后的所有位置都应被更新为0。如果遇到负数,则需要注意`max_product`和`min_product`的更新,因为一个负数乘以另一个负数可能得到一个正数,所以起始点的负数对后续的产品积有重要影响,特别是在计算路径最大积时。
🦆
为什么在填充每个格子时,需要同时考虑来自上方和左侧的路径?是否有可能只考虑一个方向会导致结果不准确?
在填充每个格子时,需要同时考虑来自上方和左侧的路径,因为这两个方向都可能提供到达当前单元格的最优路径(即最大积或最小积)。如果只考虑一个方向,则可能会忽略通过另一个方向到达该单元格时可能获得的更大的积。例如,从左侧到达的路径可能有一个很大的积,而从上方到达的路径可能因为某些负数的存在而提供一个更小或更大的积。忽略任何一个方向都可能导致不能计算出真正的最大或最小积,因此考虑所有可能的路径是确保算法准确性的关键。

相关问题