Facebook Pixel

3418. Maximum Amount of Money Robot Can Earn

Problem Description

You have a robot that needs to navigate through an m x n grid from the top-left corner (0, 0) to the bottom-right corner (m - 1, n - 1). The robot can only move right or down at each step.

Each cell in the grid contains a value coins[i][j]:

  • If coins[i][j] >= 0, the robot collects that many coins when passing through the cell
  • If coins[i][j] < 0, there's a robber in that cell who steals |coins[i][j]| coins from the robot

The robot has a special ability: it can neutralize robbers in at most 2 cells during its journey. When a robber is neutralized, they cannot steal any coins from that cell.

The robot's coin count can go negative if robbers steal more coins than the robot has collected.

Your task is to find the maximum number of coins the robot can have when it reaches the destination by choosing:

  1. The optimal path from start to finish
  2. Which robbers (if any) to neutralize along the way

For example, if the robot encounters a cell with -5 coins and chooses to neutralize the robber there, it passes through without losing 5 coins. If it doesn't neutralize the robber, it loses 5 coins (its total decreases by 5).

The goal is to return the maximum profit (total coins) the robot can achieve by strategically using its path choices and neutralization abilities.

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

Intuition

Since the robot can only move right or down, this is a path-finding problem where we need to make optimal decisions at each step. The key insight is that we have a limited resource (2 neutralizations) that we can use strategically along our path.

At each cell (i, j), we need to track not just our position but also how many neutralizations we have left. This suggests we need a state that includes three pieces of information: our row position i, column position j, and remaining neutralizations k.

For any position with k neutralizations remaining, we have choices to make:

  • We always choose between moving right or moving down (taking the path that gives us more coins)
  • If the current cell has a robber (coins[i][j] < 0) and we still have neutralizations left (k > 0), we have an additional choice: should we use a neutralization here or save it for later?

This naturally leads to a recursive approach where we explore all possible paths and neutralization strategies. The function dfs(i, j, k) represents "what's the maximum coins we can get from position (i, j) with k neutralizations left?"

The base cases are straightforward:

  • If we go out of bounds, return negative infinity (invalid path)
  • If we reach the destination (m-1, n-1), we need to handle the final cell carefully. If we have neutralizations left and there's a robber, we can neutralize them to avoid losing coins

For the recursive case, we always collect/lose the coins at the current cell and then choose the better path (right or down). If there's a robber at the current position and we have neutralizations available, we compare two strategies: neutralizing the robber here (which means we don't lose coins at this cell) versus saving the neutralization for potentially worse robbers later.

Since we'll revisit the same states multiple times through different paths, memoization helps us avoid redundant calculations, making the solution efficient.

Learn more about Dynamic Programming patterns.

Solution Approach

The solution uses memoized recursion (dynamic programming with memoization) to explore all possible paths and neutralization strategies.

We implement a recursive function dfs(i, j, k) where:

  • i and j represent the current position in the grid
  • k represents the number of neutralizations remaining (starts at 2)

Base Cases:

  1. Out of bounds: If i >= m or j >= n, we've moved outside the grid, so we return -inf to indicate this is an invalid path.

  2. Destination reached: When i == m - 1 and j == n - 1:

    • If we have neutralizations left (k > 0), we can choose to neutralize a robber if present, so we return max(coins[i][j], 0) - either collect positive coins or neutralize negative ones
    • If no neutralizations left (k == 0), we must accept whatever value is in the cell, so we return coins[i][j]

Recursive Cases:

For any other position, we calculate the maximum coins by considering our movement options:

  1. Basic movement: We always add coins[i][j] to our total and choose the better path between moving right dfs(i + 1, j, k) or down dfs(i, j + 1, k):

    ans = coins[i][j] + max(dfs(i + 1, j, k), dfs(i, j + 1, k))
  2. Neutralization option: If the current cell has a robber (coins[i][j] < 0) and we have neutralizations available (k > 0), we can choose to neutralize:

    • When neutralizing, we don't add the negative value to our total, so we compare paths from this position with one less neutralization
    • We take the maximum of: keeping the robber active (current ans) vs neutralizing and moving right dfs(i + 1, j, k - 1) vs neutralizing and moving down dfs(i, j + 1, k - 1)
    if coins[i][j] < 0 and k:
        ans = max(ans, dfs(i + 1, j, k - 1), dfs(i, j + 1, k - 1))

Memoization: The @cache decorator automatically memoizes the results, preventing redundant calculations when we encounter the same state (i, j, k) through different paths.

Final Answer: We start the recursion from the top-left corner with 2 neutralizations available: dfs(0, 0, 2).

The time complexity is O(m * n * 3) since we have at most m * n positions and 3 possible values for k (0, 1, 2). The space complexity is also O(m * n * 3) for the memoization cache.

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 a 3x3 grid:

[  2, -3,  4 ]
[ -5,  1,  2 ]
[  3, -2,  5 ]

We start at position (0,0) with 2 neutralizations available, calling dfs(0, 0, 2).

Step 1: From (0,0)

  • Current cell value: 2 (positive, so no neutralization option)
  • We collect 2 coins
  • Explore two paths:
    • Right: 2 + dfs(0, 1, 2)
    • Down: 2 + dfs(1, 0, 2)

Step 2: Exploring (0,1) with k=2

  • Current cell value: -3 (robber!)
  • Option 1: Don't neutralize, lose 3 coins: -3 + max(dfs(0, 2, 2), dfs(1, 1, 2))
  • Option 2: Neutralize (k becomes 1): 0 + max(dfs(0, 2, 1), dfs(1, 1, 1))
  • We choose the maximum of these options

Step 3: Exploring (1,0) with k=2

  • Current cell value: -5 (robber!)
  • Option 1: Don't neutralize, lose 5 coins: -5 + max(dfs(1, 1, 2), dfs(2, 0, 2))
  • Option 2: Neutralize (k becomes 1): 0 + max(dfs(1, 1, 1), dfs(2, 0, 1))

Let's trace one complete path: (0,0) → (0,1) → (0,2) → (1,2) → (2,2)

  1. Start at (0,0): collect 2 coins, total = 2
  2. Move to (0,1): robber with -3
    • Neutralize it (k=2→1), lose 0 coins, total = 2
  3. Move to (0,2): collect 4 coins, total = 6
  4. Move to (1,2): collect 2 coins, total = 8
  5. Reach (2,2): collect 5 coins, final total = 13

Another path: (0,0) → (1,0) → (1,1) → (2,1) → (2,2)

  1. Start at (0,0): collect 2 coins, total = 2
  2. Move to (1,0): robber with -5
    • Neutralize it (k=2→1), lose 0 coins, total = 2
  3. Move to (1,1): collect 1 coin, total = 3
  4. Move to (2,1): robber with -2
    • Neutralize it (k=1→0), lose 0 coins, total = 3
  5. Reach (2,2): collect 5 coins, final total = 8

The algorithm explores all such paths and neutralization combinations. Through memoization, when we encounter the same state (i, j, k) again, we reuse the computed result.

The maximum coins achievable would be returned by dfs(0, 0, 2), which in this case would be 13 from the first path shown (neutralizing the robbers at positions (0,1) and potentially saving the second neutralization).

Solution Implementation

1class Solution:
2    def maximumAmount(self, coins: List[List[int]]) -> int:
3        from functools import cache
4      
5        @cache
6        def dfs(row: int, col: int, neutralizations_left: int) -> int:
7            # Base case: out of bounds
8            if row >= rows or col >= cols:
9                return float('-inf')
10          
11            # Base case: reached destination (bottom-right corner)
12            if row == rows - 1 and col == cols - 1:
13                # If we have neutralizations left, we can choose to take 0 instead of negative value
14                if neutralizations_left > 0:
15                    return max(coins[row][col], 0)
16                else:
17                    return coins[row][col]
18          
19            # Recursive case: take current coin and move to the best next position
20            # Option 1: Take the current coin value and continue
21            current_value = coins[row][col]
22            max_value = current_value + max(
23                dfs(row + 1, col, neutralizations_left),  # Move down
24                dfs(row, col + 1, neutralizations_left)   # Move right
25            )
26          
27            # Option 2: If current coin is negative and we have neutralizations left,
28            # we can neutralize it (skip taking the negative value)
29            if current_value < 0 and neutralizations_left > 0:
30                max_value = max(
31                    max_value,
32                    dfs(row + 1, col, neutralizations_left - 1),  # Neutralize and move down
33                    dfs(row, col + 1, neutralizations_left - 1)   # Neutralize and move right
34                )
35          
36            return max_value
37      
38        # Initialize grid dimensions
39        rows, cols = len(coins), len(coins[0])
40      
41        # Start DFS from top-left corner with 2 neutralizations available
42        return dfs(0, 0, 2)
43
1class Solution {
2    // Memoization cache: dp[row][col][neutralizationsLeft]
3    private Integer[][][] dp;
4    private int[][] coins;
5    private int rows;
6    private int cols;
7
8    /**
9     * Finds the maximum amount of coins that can be collected from top-left to bottom-right.
10     * Can neutralize up to 2 negative cells (make them 0 instead of negative).
11     * 
12     * @param coins 2D grid where each cell contains coin values (can be negative)
13     * @return Maximum coins that can be collected
14     */
15    public int maximumAmount(int[][] coins) {
16        this.rows = coins.length;
17        this.cols = coins[0].length;
18        this.coins = coins;
19        // Initialize memoization array: [rows][cols][3 states for neutralizations (0, 1, 2)]
20        this.dp = new Integer[rows][cols][3];
21      
22        // Start DFS from top-left corner with 2 neutralizations available
23        return dfs(0, 0, 2);
24    }
25
26    /**
27     * Recursive DFS with memoization to find maximum coins from current position.
28     * 
29     * @param row Current row position
30     * @param col Current column position
31     * @param neutralizationsLeft Number of neutralizations still available
32     * @return Maximum coins collectible from current position to bottom-right
33     */
34    private int dfs(int row, int col, int neutralizationsLeft) {
35        // Base case: out of bounds
36        if (row >= rows || col >= cols) {
37            return Integer.MIN_VALUE / 2;  // Return very small value to avoid this path
38        }
39      
40        // Check memoization cache
41        if (dp[row][col][neutralizationsLeft] != null) {
42            return dp[row][col][neutralizationsLeft];
43        }
44      
45        // Base case: reached destination (bottom-right corner)
46        if (row == rows - 1 && col == cols - 1) {
47            // If we have neutralizations left, we can choose to neutralize negative values
48            return neutralizationsLeft > 0 ? Math.max(0, coins[row][col]) : coins[row][col];
49        }
50      
51        // Option 1: Collect current coin and move to next cell (right or down)
52        int maxCoins = coins[row][col] + Math.max(
53            dfs(row + 1, col, neutralizationsLeft),     // Move down
54            dfs(row, col + 1, neutralizationsLeft)       // Move right
55        );
56      
57        // Option 2: If current cell is negative and we have neutralizations left,
58        // we can neutralize it (skip collecting negative coins)
59        if (coins[row][col] < 0 && neutralizationsLeft > 0) {
60            maxCoins = Math.max(maxCoins, Math.max(
61                dfs(row + 1, col, neutralizationsLeft - 1),     // Neutralize and move down
62                dfs(row, col + 1, neutralizationsLeft - 1)       // Neutralize and move right
63            ));
64        }
65      
66        // Store result in cache and return
67        dp[row][col][neutralizationsLeft] = maxCoins;
68        return maxCoins;
69    }
70}
71
1class Solution {
2public:
3    int maximumAmount(vector<vector<int>>& coins) {
4        int rows = coins.size();
5        int cols = coins[0].size();
6      
7        // dp[i][j][k] = maximum amount starting from (i,j) with k neutralizations remaining
8        // Initialize with -1 to indicate unvisited state
9        vector<vector<vector<int>>> dp(rows, vector<vector<int>>(cols, vector<int>(3, -1)));
10      
11        // Recursive DFS function with memoization
12        auto dfs = [&](this auto&& dfs, int row, int col, int neutralizationsLeft) -> int {
13            // Base case: out of bounds
14            if (row >= rows || col >= cols) {
15                return INT_MIN / 2;  // Return very small value to avoid this path
16            }
17          
18            // Return memoized result if already computed
19            if (dp[row][col][neutralizationsLeft] != -1) {
20                return dp[row][col][neutralizationsLeft];
21            }
22          
23            // Base case: reached destination (bottom-right corner)
24            if (row == rows - 1 && col == cols - 1) {
25                // If we have neutralizations left, we can choose to neutralize negative values
26                return neutralizationsLeft > 0 ? max(0, coins[row][col]) : coins[row][col];
27            }
28          
29            // Option 1: Take current cell's value and move to next cell
30            int maxAmount = coins[row][col] + max(
31                dfs(row + 1, col, neutralizationsLeft),      // Move down
32                dfs(row, col + 1, neutralizationsLeft)       // Move right
33            );
34          
35            // Option 2: If current cell is negative and we have neutralizations left,
36            // we can skip this cell's value (neutralize it)
37            if (coins[row][col] < 0 && neutralizationsLeft > 0) {
38                maxAmount = max({
39                    maxAmount,
40                    dfs(row + 1, col, neutralizationsLeft - 1),  // Move down with neutralization
41                    dfs(row, col + 1, neutralizationsLeft - 1)   // Move right with neutralization
42                });
43            }
44          
45            // Memoize and return the result
46            return dp[row][col][neutralizationsLeft] = maxAmount;
47        };
48      
49        // Start DFS from top-left corner (0, 0) with 2 neutralizations available
50        return dfs(0, 0, 2);
51    }
52};
53
1/**
2 * Finds the maximum amount of coins that can be collected from top-left to bottom-right
3 * @param coins - 2D array representing the grid of coins (positive or negative values)
4 * @returns Maximum amount of coins that can be collected
5 */
6function maximumAmount(coins: number[][]): number {
7    const rows: number = coins.length;
8    const cols: number = coins[0].length;
9  
10    // Initialize memoization table: dp[row][col][neutralizations_remaining]
11    // Each cell stores the maximum coins collectible from that position with k neutralizations left
12    const memoTable: number[][][] = Array.from({ length: rows }, () =>
13        Array.from({ length: cols }, () => Array(3).fill(-Infinity))
14    );
15  
16    /**
17     * Depth-first search with memoization to find maximum coins
18     * @param row - Current row position
19     * @param col - Current column position
20     * @param neutralizationsLeft - Number of neutralizations remaining (can skip negative cells)
21     * @returns Maximum coins collectible from current position to destination
22     */
23    const dfs = (row: number, col: number, neutralizationsLeft: number): number => {
24        // Check if out of bounds
25        if (row >= rows || col >= cols) {
26            return -Infinity;
27        }
28      
29        // Return memoized result if already calculated
30        if (memoTable[row][col][neutralizationsLeft] !== -Infinity) {
31            return memoTable[row][col][neutralizationsLeft];
32        }
33      
34        // Base case: reached destination (bottom-right corner)
35        if (row === rows - 1 && col === cols - 1) {
36            // If we have neutralizations left, we can choose to take 0 instead of negative value
37            return neutralizationsLeft > 0 ? Math.max(0, coins[row][col]) : coins[row][col];
38        }
39      
40        // Calculate maximum by taking current cell and moving down or right
41        let maxCoins: number = coins[row][col] + Math.max(
42            dfs(row + 1, col, neutralizationsLeft),  // Move down
43            dfs(row, col + 1, neutralizationsLeft)   // Move right
44        );
45      
46        // If current cell is negative and we have neutralizations left, 
47        // we can skip this cell's value (neutralize it)
48        if (coins[row][col] < 0 && neutralizationsLeft > 0) {
49            maxCoins = Math.max(
50                maxCoins,
51                dfs(row + 1, col, neutralizationsLeft - 1),  // Skip current, move down
52                dfs(row, col + 1, neutralizationsLeft - 1)   // Skip current, move right
53            );
54        }
55      
56        // Memoize and return the result
57        memoTable[row][col][neutralizationsLeft] = maxCoins;
58        return maxCoins;
59    };
60  
61    // Start from top-left corner (0, 0) with 2 neutralizations available
62    return dfs(0, 0, 2);
63}
64

Time and Space Complexity

The time complexity is O(m × n × k), where m is the number of rows, n is the number of columns in the 2D array coins, and k is the number of conversion opportunities (which is 3 in this problem, since we start with k=2 and can use at most 2 conversions).

The recursive function dfs(i, j, k) explores states defined by:

  • Position (i, j) in the grid: m × n possible positions
  • Number of remaining conversions k: can be 0, 1, or 2 (total of 3 values)

Due to the @cache decorator, each unique state (i, j, k) is computed only once. Therefore, the total number of states is m × n × 3, giving us a time complexity of O(m × n × k).

The space complexity is also O(m × n × k) due to:

  1. The memoization cache storing up to m × n × 3 states
  2. The recursion stack depth, which in the worst case can be O(m + n) (moving right then down or down then right), but this is dominated by the cache space

Since k = 3 is a constant in this problem, both complexities can also be expressed as O(m × n).

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

Common Pitfalls

Pitfall 1: Incorrect Handling of Neutralization Logic

The Problem: A common mistake is treating neutralization as "converting negative to positive" rather than "converting negative to zero". For example:

# INCORRECT: Converting negative to positive
if current_value < 0 and neutralizations_left > 0:
    max_value = max(
        max_value,
        abs(current_value) + dfs(row + 1, col, neutralizations_left - 1),
        abs(current_value) + dfs(row, col + 1, neutralizations_left - 1)
    )

This would incorrectly give us coins when neutralizing a robber, when we should simply avoid losing coins.

The Solution: When neutralizing, we skip adding the negative value entirely (equivalent to adding 0):

# CORRECT: Neutralizing means we don't lose coins (add 0 instead of negative)
if current_value < 0 and neutralizations_left > 0:
    max_value = max(
        max_value,
        dfs(row + 1, col, neutralizations_left - 1),  # 0 + next state
        dfs(row, col + 1, neutralizations_left - 1)   # 0 + next state
    )

Pitfall 2: Forgetting to Handle the Destination Cell Specially

The Problem: Not considering that we might want to neutralize a robber at the destination cell itself:

# INCORRECT: Always taking the destination value
if row == rows - 1 and col == cols - 1:
    return coins[row][col]  # Always forced to take this value

The Solution: Check if we have neutralizations available at the destination:

# CORRECT: Can neutralize at destination if we have neutralizations left
if row == rows - 1 and col == cols - 1:
    if neutralizations_left > 0:
        return max(coins[row][col], 0)  # Can neutralize if negative
    else:
        return coins[row][col]  # Must take the value

Pitfall 3: Using Greedy Neutralization Strategy

The Problem: Automatically neutralizing the most negative values encountered:

# INCORRECT: Greedily neutralizing any negative value
if current_value < 0 and neutralizations_left > 0:
    # Always neutralize negative values
    return max(
        dfs(row + 1, col, neutralizations_left - 1),
        dfs(row, col + 1, neutralizations_left - 1)
    )

The Solution: Consider both options - neutralizing AND not neutralizing - to find the optimal strategy:

# CORRECT: Compare both neutralizing and not neutralizing
max_value = current_value + max(...)  # Option 1: Don't neutralize

if current_value < 0 and neutralizations_left > 0:
    max_value = max(
        max_value,  # Keep option 1 (don't neutralize)
        dfs(row + 1, col, neutralizations_left - 1),  # Option 2: Neutralize
        dfs(row, col + 1, neutralizations_left - 1)
    )

Sometimes it's better to save neutralizations for larger negative values later in the path rather than using them on smaller negative values early on.

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

How many times is a tree node visited in a depth first search?


Recommended Readings

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

Load More