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:
- The optimal path from start to finish
- 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.
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
andj
represent the current position in the gridk
represents the number of neutralizations remaining (starts at 2)
Base Cases:
-
Out of bounds: If
i >= m
orj >= n
, we've moved outside the grid, so we return-inf
to indicate this is an invalid path. -
Destination reached: When
i == m - 1
andj == n - 1
:- If we have neutralizations left (
k > 0
), we can choose to neutralize a robber if present, so we returnmax(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 returncoins[i][j]
- If we have neutralizations left (
Recursive Cases:
For any other position, we calculate the maximum coins by considering our movement options:
-
Basic movement: We always add
coins[i][j]
to our total and choose the better path between moving rightdfs(i + 1, j, k)
or downdfs(i, j + 1, k)
:ans = coins[i][j] + max(dfs(i + 1, j, k), dfs(i, j + 1, k))
-
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 rightdfs(i + 1, j, k - 1)
vs neutralizing and moving downdfs(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 EvaluatorExample 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)
- Right:
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)
- Start at (0,0): collect 2 coins, total = 2
- Move to (0,1): robber with -3
- Neutralize it (k=2→1), lose 0 coins, total = 2
- Move to (0,2): collect 4 coins, total = 6
- Move to (1,2): collect 2 coins, total = 8
- Reach (2,2): collect 5 coins, final total = 13
Another path: (0,0) → (1,0) → (1,1) → (2,1) → (2,2)
- Start at (0,0): collect 2 coins, total = 2
- Move to (1,0): robber with -5
- Neutralize it (k=2→1), lose 0 coins, total = 2
- Move to (1,1): collect 1 coin, total = 3
- Move to (2,1): robber with -2
- Neutralize it (k=1→0), lose 0 coins, total = 3
- 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:
- The memoization cache storing up to
m × n × 3
states - 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.
How many times is a tree node visited in a depth first search?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!