Facebook Pixel

1594. Maximum Non Negative Product in a Matrix

Problem Description

You have a m x n matrix filled with integers. Your task is to find a path from the top-left corner (0, 0) to the bottom-right corner (m-1, n-1) that gives the maximum non-negative product.

Movement Rules:

  • Start at position (0, 0) (top-left corner)
  • You can only move right or down at each step
  • End at position (m-1, n-1) (bottom-right corner)

Product Calculation:

  • The product of a path is calculated by multiplying all the numbers in the cells you visit along that path
  • You want to find the path that gives the maximum product that is non-negative (≥ 0)

Return Value:

  • If the maximum product is non-negative, return it modulo 10^9 + 7
  • If the maximum product is negative (all possible paths result in negative products), return -1

Important Note: The modulo operation is applied after finding the maximum product, not during the calculation.

Example: If you have a grid like:

[2, 1, 3]
[0, 1, -2]

Some possible paths from (0,0) to (1,2) are:

  • Path 1: 2 → 1 → 3 → -2 with product = 2 × 1 × 3 × (-2) = -12
  • Path 2: 2 → 0 → 1 → -2 with product = 2 × 0 × 1 × (-2) = 0

The maximum non-negative product would be 0 in this case.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When dealing with products, we need to consider that negative numbers can become positive when multiplied by another negative number. This means a very small (most negative) value could potentially become the largest positive value if we multiply it by a negative number later in the path.

Consider this scenario: if we have a path with product -100 and we encounter a cell with value -2, the product becomes 200. Meanwhile, a path with product 50 encountering the same -2 would only result in -100.

This leads us to a key insight: we need to track both the minimum and maximum products at each cell, because:

  • The minimum product (most negative) could become the maximum when multiplied by a negative number
  • The maximum product could become the minimum when multiplied by a negative number

For each cell (i, j), we need to know:

  1. The minimum product possible to reach this cell
  2. The maximum product possible to reach this cell

When we move to a new cell with value v:

  • If v ≥ 0:
    • The new minimum = (smaller of the two previous minimums) × v
    • The new maximum = (larger of the two previous maximums) × v
  • If v < 0:
    • The new minimum = (larger of the two previous maximums) × v (positive becomes negative)
    • The new maximum = (smaller of the two previous minimums) × v (negative becomes positive)

By maintaining both extremes throughout our path, we ensure we don't miss any opportunity where a negative intermediate result could lead to the optimal positive result later.

The dynamic programming approach naturally fits here because:

  • Each cell's result only depends on its immediate predecessors (cell above or cell to the left)
  • We can build up the solution incrementally from top-left to bottom-right
  • The subproblems (finding min/max products to reach each cell) overlap and can be reused

Learn more about Dynamic Programming patterns.

Solution Approach

We implement a dynamic programming solution using a 3D array dp[i][j][k] where:

  • dp[i][j][0] stores the minimum product to reach cell (i, j)
  • dp[i][j][1] stores the maximum product to reach cell (i, j)

Step 1: Initialize the DP array

dp = [[[grid[0][0]] * 2 for _ in range(n)] for _ in range(m)]

The starting cell (0, 0) has both minimum and maximum values equal to grid[0][0].

Step 2: Fill the first column

for i in range(1, m):
    dp[i][0] = [dp[i - 1][0][0] * grid[i][0]] * 2

For cells in the first column, we can only arrive from above. Since there's only one path, both min and max are the same: the product of all cells from (0, 0) to (i, 0).

Step 3: Fill the first row

for j in range(1, n):
    dp[0][j] = [dp[0][j - 1][0] * grid[0][j]] * 2

Similarly, for the first row, we can only arrive from the left, so both min and max are the cumulative product.

Step 4: Fill the rest of the grid For each cell (i, j) where i ≥ 1 and j ≥ 1:

We can arrive from either (i-1, j) (above) or (i, j-1) (left). The current cell value is v = grid[i][j].

If v ≥ 0 (non-negative):

dp[i][j][0] = min(dp[i - 1][j][0], dp[i][j - 1][0]) * v
dp[i][j][1] = max(dp[i - 1][j][1], dp[i][j - 1][1]) * v
  • Minimum stays minimum when multiplied by positive
  • Maximum stays maximum when multiplied by positive

If v < 0 (negative):

dp[i][j][0] = max(dp[i - 1][j][1], dp[i][j - 1][1]) * v
dp[i][j][1] = min(dp[i - 1][j][0], dp[i][j - 1][0]) * v
  • The largest positive becomes the smallest (most negative) when multiplied by negative
  • The smallest (most negative) becomes the largest positive when multiplied by negative

Step 5: Return the result

ans = dp[-1][-1][1]
return -1 if ans < 0 else ans % mod

The maximum product to reach the bottom-right corner is stored in dp[m-1][n-1][1]. If it's negative, return -1; otherwise, return the value modulo 10^9 + 7.

Time Complexity: O(m × n) - we visit each cell once Space Complexity: O(m × n) - for the DP array storing min/max for each cell

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate the solution approach.

Consider this 3×3 grid:

[-1,  2,  3]
[ 4, -5,  6]
[ 7,  8, -9]

We'll track both minimum and maximum products at each cell using our DP approach.

Step 1: Initialize

  • dp[0][0]: min = -1, max = -1 (starting cell value)

Step 2: Fill first row

  • dp[0][1]: Only reachable from left
    • min = max = -1 × 2 = -2
  • dp[0][2]: Only reachable from left
    • min = max = -2 × 3 = -6

Step 3: Fill first column

  • dp[1][0]: Only reachable from above
    • min = max = -1 × 4 = -4
  • dp[2][0]: Only reachable from above
    • min = max = -4 × 7 = -28

Step 4: Fill remaining cells

For dp[1][1] (value = -5, negative):

  • From above: min = -2, max = -2
  • From left: min = -4, max = -4
  • Since -5 is negative, we swap min/max logic:
    • New min = max(-2, -4) × (-5) = -2 × (-5) = 10
    • New max = min(-2, -4) × (-5) = -4 × (-5) = 20

For dp[1][2] (value = 6, positive):

  • From above: min = -6, max = -6
  • From left: min = 10, max = 20
  • Since 6 is positive:
    • New min = min(-6, 10) × 6 = -6 × 6 = -36
    • New max = max(-6, 20) × 6 = 20 × 6 = 120

For dp[2][1] (value = 8, positive):

  • From above: min = 10, max = 20
  • From left: min = -28, max = -28
  • Since 8 is positive:
    • New min = min(10, -28) × 8 = -28 × 8 = -224
    • New max = max(20, -28) × 8 = 20 × 8 = 160

For dp[2][2] (value = -9, negative):

  • From above: min = -36, max = 120
  • From left: min = -224, max = 160
  • Since -9 is negative, we swap:
    • New min = max(120, 160) × (-9) = 160 × (-9) = -1440
    • New max = min(-36, -224) × (-9) = -224 × (-9) = 2016

Step 5: Return result The maximum product at dp[2][2] is 2016, which is non-negative. Return: 2016 % (10^9 + 7) = 2016

The optimal path that gives us 2016 is: -1 → 4 → -5 → 6 → 8 → -9 Product: (-1) × 4 × (-5) × 8 × (-9) = 20 × 8 × (-9) = -1440 (Wait, this doesn't match!)

Let me recalculate the path correctly: Actually, the path is: -1 → 4 → 7 → 8 → -9 Product: (-1) × 4 × 7 × 8 × (-9) = -28 × 8 × (-9) = 2016 ✓

This example demonstrates how tracking both minimum and maximum values is crucial - the negative minimum (-224) at cell (2,1) became our maximum (2016) when multiplied by the negative value (-9) at the destination.

Solution Implementation

1class Solution:
2    def maxProductPath(self, grid: List[List[int]]) -> int:
3        # Get grid dimensions
4        rows, cols = len(grid), len(grid[0])
5        MOD = 10**9 + 7
6      
7        # Initialize DP table: dp[i][j] = [min_product, max_product] at cell (i, j)
8        # Each cell stores both minimum and maximum products to handle negative numbers
9        dp = [[[grid[0][0], grid[0][0]] for _ in range(cols)] for _ in range(rows)]
10      
11        # Initialize first column (can only come from above)
12        for row in range(1, rows):
13            prev_product = dp[row - 1][0][0]
14            current_value = grid[row][0]
15            dp[row][0][0] = prev_product * current_value  # min product
16            dp[row][0][1] = prev_product * current_value  # max product
17      
18        # Initialize first row (can only come from left)
19        for col in range(1, cols):
20            prev_product = dp[0][col - 1][0]
21            current_value = grid[0][col]
22            dp[0][col][0] = prev_product * current_value  # min product
23            dp[0][col][1] = prev_product * current_value  # max product
24      
25        # Fill the DP table for remaining cells
26        for row in range(1, rows):
27            for col in range(1, cols):
28                current_value = grid[row][col]
29              
30                if current_value >= 0:
31                    # For non-negative values:
32                    # - Minimum comes from multiplying with previous minimum
33                    # - Maximum comes from multiplying with previous maximum
34                    min_from_above = dp[row - 1][col][0]
35                    min_from_left = dp[row][col - 1][0]
36                    dp[row][col][0] = min(min_from_above, min_from_left) * current_value
37                  
38                    max_from_above = dp[row - 1][col][1]
39                    max_from_left = dp[row][col - 1][1]
40                    dp[row][col][1] = max(max_from_above, max_from_left) * current_value
41                else:
42                    # For negative values:
43                    # - Minimum comes from multiplying with previous maximum (sign flip)
44                    # - Maximum comes from multiplying with previous minimum (sign flip)
45                    max_from_above = dp[row - 1][col][1]
46                    max_from_left = dp[row][col - 1][1]
47                    dp[row][col][0] = max(max_from_above, max_from_left) * current_value
48                  
49                    min_from_above = dp[row - 1][col][0]
50                    min_from_left = dp[row][col - 1][0]
51                    dp[row][col][1] = min(min_from_above, min_from_left) * current_value
52      
53        # Get the maximum product at the bottom-right cell
54        max_product = dp[-1][-1][1]
55      
56        # Return -1 if the maximum product is negative, otherwise return modulo result
57        return -1 if max_product < 0 else max_product % MOD
58
1class Solution {
2    // Modulo value for the result
3    private static final int MOD = (int) 1e9 + 7;
4
5    public int maxProductPath(int[][] grid) {
6        int rows = grid.length;
7        int cols = grid[0].length;
8      
9        // dp[i][j][0] stores the minimum product to reach cell (i, j)
10        // dp[i][j][1] stores the maximum product to reach cell (i, j)
11        // We track both min and max because negative numbers can flip the relationship
12        long[][][] dp = new long[rows][cols][2];
13      
14        // Initialize starting cell with its value for both min and max
15        dp[0][0][0] = grid[0][0];
16        dp[0][0][1] = grid[0][0];
17      
18        // Initialize first column (can only come from above)
19        for (int i = 1; i < rows; i++) {
20            dp[i][0][0] = dp[i - 1][0][0] * grid[i][0];
21            dp[i][0][1] = dp[i - 1][0][1] * grid[i][0];
22        }
23      
24        // Initialize first row (can only come from left)
25        for (int j = 1; j < cols; j++) {
26            dp[0][j][0] = dp[0][j - 1][0] * grid[0][j];
27            dp[0][j][1] = dp[0][j - 1][1] * grid[0][j];
28        }
29      
30        // Fill the dp table for remaining cells
31        for (int i = 1; i < rows; i++) {
32            for (int j = 1; j < cols; j++) {
33                int currentValue = grid[i][j];
34              
35                if (currentValue >= 0) {
36                    // For positive values:
37                    // - Minimum stays minimum after multiplication
38                    // - Maximum stays maximum after multiplication
39                    dp[i][j][0] = Math.min(dp[i - 1][j][0], dp[i][j - 1][0]) * currentValue;
40                    dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i][j - 1][1]) * currentValue;
41                } else {
42                    // For negative values:
43                    // - Previous maximum becomes new minimum after multiplication
44                    // - Previous minimum becomes new maximum after multiplication
45                    dp[i][j][0] = Math.max(dp[i - 1][j][1], dp[i][j - 1][1]) * currentValue;
46                    dp[i][j][1] = Math.min(dp[i - 1][j][0], dp[i][j - 1][0]) * currentValue;
47                }
48            }
49        }
50      
51        // Get the maximum product at the destination cell
52        long maxProduct = dp[rows - 1][cols - 1][1];
53      
54        // Return -1 if the maximum product is negative, otherwise return modulo result
55        return maxProduct < 0 ? -1 : (int) (maxProduct % MOD);
56    }
57}
58
1using ll = long long;
2const int MOD = 1e9 + 7;
3
4class Solution {
5public:
6    int maxProductPath(vector<vector<int>>& grid) {
7        int rows = grid.size();
8        int cols = grid[0].size();
9      
10        // dp[i][j][0] stores minimum product to reach (i,j)
11        // dp[i][j][1] stores maximum product to reach (i,j)
12        // We track both min and max because negative numbers can flip min to max
13        vector<vector<vector<ll>>> dp(rows, vector<vector<ll>>(cols, vector<ll>(2)));
14      
15        // Initialize starting point
16        dp[0][0][0] = grid[0][0];  // min at (0,0)
17        dp[0][0][1] = grid[0][0];  // max at (0,0)
18      
19        // Initialize first column (can only come from above)
20        for (int i = 1; i < rows; ++i) {
21            dp[i][0][0] = dp[i - 1][0][0] * grid[i][0];
22            dp[i][0][1] = dp[i - 1][0][1] * grid[i][0];
23        }
24      
25        // Initialize first row (can only come from left)
26        for (int j = 1; j < cols; ++j) {
27            dp[0][j][0] = dp[0][j - 1][0] * grid[0][j];
28            dp[0][j][1] = dp[0][j - 1][1] * grid[0][j];
29        }
30      
31        // Fill the dp table for remaining cells
32        for (int i = 1; i < rows; ++i) {
33            for (int j = 1; j < cols; ++j) {
34                int currentValue = grid[i][j];
35              
36                if (currentValue >= 0) {
37                    // For positive numbers:
38                    // - minimum stays minimum after multiplication
39                    // - maximum stays maximum after multiplication
40                    dp[i][j][0] = min(dp[i - 1][j][0], dp[i][j - 1][0]) * currentValue;
41                    dp[i][j][1] = max(dp[i - 1][j][1], dp[i][j - 1][1]) * currentValue;
42                } else {
43                    // For negative numbers:
44                    // - maximum becomes minimum after multiplication
45                    // - minimum becomes maximum after multiplication
46                    dp[i][j][0] = max(dp[i - 1][j][1], dp[i][j - 1][1]) * currentValue;
47                    dp[i][j][1] = min(dp[i - 1][j][0], dp[i][j - 1][0]) * currentValue;
48                }
49            }
50        }
51      
52        // Get the maximum product at destination
53        ll maxProduct = dp[rows - 1][cols - 1][1];
54      
55        // Return -1 if the maximum product is negative, otherwise return modulo result
56        return maxProduct < 0 ? -1 : static_cast<int>(maxProduct % MOD);
57    }
58};
59
1type ll = number;
2const MOD = 1e9 + 7;
3
4function maxProductPath(grid: number[][]): number {
5    const rows = grid.length;
6    const cols = grid[0].length;
7  
8    // dp[i][j][0] stores minimum product to reach cell (i, j)
9    // dp[i][j][1] stores maximum product to reach cell (i, j)
10    // We track both min and max because negative numbers can flip min to max
11    const dp: number[][][] = Array(rows).fill(null).map(() => 
12        Array(cols).fill(null).map(() => Array(2).fill(0))
13    );
14  
15    // Initialize starting point at (0, 0)
16    dp[0][0][0] = grid[0][0];  // minimum at (0, 0)
17    dp[0][0][1] = grid[0][0];  // maximum at (0, 0)
18  
19    // Initialize first column (can only be reached from above)
20    for (let i = 1; i < rows; i++) {
21        dp[i][0][0] = dp[i - 1][0][0] * grid[i][0];
22        dp[i][0][1] = dp[i - 1][0][1] * grid[i][0];
23    }
24  
25    // Initialize first row (can only be reached from left)
26    for (let j = 1; j < cols; j++) {
27        dp[0][j][0] = dp[0][j - 1][0] * grid[0][j];
28        dp[0][j][1] = dp[0][j - 1][1] * grid[0][j];
29    }
30  
31    // Fill the dp table for remaining cells
32    for (let i = 1; i < rows; i++) {
33        for (let j = 1; j < cols; j++) {
34            const currentValue = grid[i][j];
35          
36            if (currentValue >= 0) {
37                // For positive or zero values:
38                // - minimum stays minimum after multiplication
39                // - maximum stays maximum after multiplication
40                dp[i][j][0] = Math.min(dp[i - 1][j][0], dp[i][j - 1][0]) * currentValue;
41                dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i][j - 1][1]) * currentValue;
42            } else {
43                // For negative values:
44                // - maximum becomes minimum after multiplication (sign flips)
45                // - minimum becomes maximum after multiplication (sign flips)
46                dp[i][j][0] = Math.max(dp[i - 1][j][1], dp[i][j - 1][1]) * currentValue;
47                dp[i][j][1] = Math.min(dp[i - 1][j][0], dp[i][j - 1][0]) * currentValue;
48            }
49        }
50    }
51  
52    // Get the maximum product at destination (bottom-right corner)
53    const maxProduct = dp[rows - 1][cols - 1][1];
54  
55    // Return -1 if the maximum product is negative, otherwise return modulo result
56    return maxProduct < 0 ? -1 : maxProduct % MOD;
57}
58

Time and Space Complexity

Time Complexity: O(m * n) where m is the number of rows and n is the number of columns in the grid.

The algorithm iterates through each cell of the grid exactly once after initialization:

  • Initializing the first row takes O(m) time
  • Initializing the first column takes O(n) time
  • The nested loops iterate through the remaining (m-1) * (n-1) cells, performing constant time operations for each cell
  • Total time: O(m) + O(n) + O(m * n) = O(m * n)

Space Complexity: O(m * n)

The space is dominated by the 3D DP array dp which has dimensions m × n × 2:

  • Each cell in the grid corresponds to an entry in dp that stores two values (minimum and maximum products)
  • Total space used: m * n * 2 = O(m * n)
  • Additional variables (m, n, mod, v, ans) use O(1) space
  • Overall space complexity: O(m * n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Handling of Zero Values

Pitfall: When a cell contains 0, the product becomes 0, which might seem straightforward. However, a common mistake is not recognizing that 0 can actually be the optimal path when all other paths lead to negative products. Some implementations might incorrectly skip or special-case zeros.

Solution: Treat 0 like any other non-negative number. When multiplying by 0, both min and max become 0, which correctly propagates through the DP table.

2. Applying Modulo During Computation

Pitfall: A critical error is applying the modulo operation during the DP computation:

# WRONG - Don't do this!
dp[i][j][1] = (max(dp[i-1][j][1], dp[i][j-1][1]) * v) % MOD

This breaks the algorithm because modulo changes the actual values, making comparisons invalid. For example, if one path gives 10^9 + 8 and another gives 3, after modulo they become 1 and 3 respectively, incorrectly suggesting the second path is better.

Solution: Only apply modulo at the very end, after finding the maximum product:

# CORRECT
max_product = dp[-1][-1][1]
return -1 if max_product < 0 else max_product % MOD

3. Integer Overflow in Languages with Fixed Integer Size

Pitfall: In languages like Java or C++, multiplying large numbers can cause integer overflow. The product can grow exponentially with grid size, easily exceeding the bounds of standard integer types.

Solution: Use appropriate data types:

  • In Python: No issue as integers have arbitrary precision
  • In Java: Use long or BigInteger
  • In C++: Use long long or implement custom handling

4. Incorrect Sign Flip Logic for Negative Numbers

Pitfall: When encountering a negative number, forgetting to swap min/max logic:

# WRONG - Using same logic as positive numbers
if v < 0:
    dp[i][j][0] = min(dp[i-1][j][0], dp[i][j-1][0]) * v  # Wrong!
    dp[i][j][1] = max(dp[i-1][j][1], dp[i][j-1][1]) * v  # Wrong!

Solution: Remember that multiplying by a negative number flips the ordering:

# CORRECT
if v < 0:
    dp[i][j][0] = max(dp[i-1][j][1], dp[i][j-1][1]) * v  # Max becomes min
    dp[i][j][1] = min(dp[i-1][j][0], dp[i][j-1][0]) * v  # Min becomes max

5. Not Tracking Both Minimum and Maximum

Pitfall: Only tracking the maximum product, thinking it's sufficient:

# WRONG - Only tracking maximum
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) * grid[i][j]

This fails when negative numbers are involved because a very negative number (minimum) can become very positive (maximum) when multiplied by another negative number.

Solution: Always track both minimum and maximum at each cell to handle all possible sign combinations correctly.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More