3418. Maximum Amount of Money Robot Can Earn
Problem Description
You are given an m x n
grid. A robot starts at the top-left corner of the grid (0, 0)
and wants to reach the bottom-right corner (m - 1, n - 1)
. The robot can move either right or down at any point in time.
The grid contains a value coins[i][j]
in each cell:
- If
coins[i][j] >= 0
, the robot gains that many coins. - If
coins[i][j] < 0
, the robot encounters a robber, and the robber steals the absolute value ofcoins[i][j]
coins.
The robot has a special ability to neutralize robbers in at most 2 cells on its path, preventing them from stealing coins in those cells.
Note: The robot's total coins can be negative.
Return the maximum profit the robot can gain on the route.
Intuition
To solve the problem, consider the robot's objective to maximize coin collection while traversing from the top-left to the bottom-right corner of the grid. The robot can negate the effect of robbers in up to two cells, which complicates the decision-making process. The dynamic programming approach is useful here, utilizing recursive depth-first search (DFS) with memoization to efficiently explore possible paths.
We define dfs(i, j, k)
as a function representing the maximum coins collected starting from cell (i, j)
with k
opportunities left to neutralize robbers. The robot can only move right or down, so dfs(i, j, k)
depends on possible maximum coins from positions (i+1, j)
and (i, j+1)
.
-
If
i >= m
orj >= n
, the robot is out of bounds, leading to an invalid path. Return a very small value to indicate this. -
If
i = m - 1
andj = n - 1
, the robot reaches the grid's bottom-right corner. Consider conversion of the robber if possible. Return the maximum betweencoins[i][j]
and0
if conversions are left, else returncoins[i][j]
. -
For cells with robbers (
coins[i][j] < 0
), decide whether to neutralize the robber or not:- If neutralizing is possible (
k > 0
), explore both with and without neutralization to determine the maximum benefit.
- If neutralizing is possible (
-
Use memoization to store results of computed states
(i, j, k)
to avoid redundant computations and improve efficiency.
The described approach efficiently explores the maximum profit path considering the special ability to bypass robbers in select positions.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution uses a Dynamic Programming (DP) approach with a recursive depth-first search (DFS) to explore all potential paths the robot can take across the grid, tracking the maximum profit obtainable. Here's a detailed walkthrough of the implementation:
-
Define the Recursive Function: We define a function
dfs(i, j, k)
wherei
andj
represent the current row and column in the grid, respectively, andk
is the number of special opportunities remaining to neutralize one bandit cell cost. The function computes the maximum coins obtainable from the current cell onwards. -
Base Cases:
- If the robot moves out of bounds (
i >= m
orj >= n
), return a very small number (negative infinity) to signify an invalid path. - If the robot reaches the destination cell
(m - 1, n - 1)
, returnmax(0, coins[i][j])
if neutralization opportunities are available (k > 0
), else returncoins[i][j]
.
- If the robot moves out of bounds (
-
Recursive Exploration:
- Compute the potential maximum profit from moving right (
dfs(i, j+1, k)
) or down (dfs(i+1, j, k)
). - Include the coins value of the current cell
coins[i][j]
in the computation.
- Compute the potential maximum profit from moving right (
-
Handling Bandit Cells:
- If the current cell has a negative coins value (indicating a bandit), consider two scenarios:
- Not using a neutralization opportunity: Continue the computation normally.
- Using a neutralization opportunity (if
k > 0
): Compute the maximum profit from both possible moves withk - 1
opportunities remaining.
- If the current cell has a negative coins value (indicating a bandit), consider two scenarios:
-
Memoization: Use Python's
@cache
decorator to remember results of computed states, avoiding repeated calculations for already explored paths with the same conditions.
Here's the implementation encapsulating the above logic:
class Solution:
def maximumAmount(self, coins: List[List[int]]) -> int:
@cache
def dfs(i: int, j: int, k: int) -> int:
if i >= m or j >= n:
return -inf
if i == m - 1 and j == n - 1:
return max(coins[i][j], 0) if k else coins[i][j]
ans = coins[i][j] + max(dfs(i + 1, j, k), dfs(i, j + 1, k))
if coins[i][j] < 0 and k:
ans = max(ans, dfs(i + 1, j, k - 1), dfs(i, j + 1, k - 1))
return ans
m, n = len(coins), len(coins[0])
return dfs(0, 0, 2)
This solution carefully balances choices at each step and uses memoization to efficiently find the path yielding the highest profit.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's consider a simple 3x3 grid as an example:
coins = [ [0, 1, -2], [1, -3, 2], [-1, 4, 5] ]
In this grid, the robot starts at the top-left corner (0, 0)
and wants to reach the bottom-right corner (2, 2)
. The robot can neutralize the negative impact of robbers up to two times. The goal is to maximize the total coins collected.
-
Start at (0, 0):
- Begin at
(0, 0)
with2
neutralization opportunities. - The cell
(0, 0)
has0
coins, so we move onward.
- Begin at
-
Move to (0, 1):
- Choose to move right to
(0, 1)
which contains1
coin. - Total coins are now
1
.
- Choose to move right to
-
Move to (0, 2):
- From
(0, 1)
, move to(0, 2)
encountering a robber with-2
coins. - Decide to neutralize the robber, utilizing one special ability (
k = 1
left). - Total remains
1
coin, as the robber's effect is negated.
- From
-
Move to (1, 2):
- From
(0, 2)
, move down to(1, 2)
gaining2
coins. - Total coins become
3
.
- From
-
Move to (2, 2):
- Move to
(2, 2)
, the destination, with5
coins. - Total coins become
8
. - The path is reached with one special ability unused (
k = 1
).
- Move to
Using dynamic programming and memoization, the function maximizes the coins by considering alternative paths and neutralization choices. After evaluating possible paths and using the special abilities optimally, the final profit is determined. In this example, the maximum profit the robot could gain on the route is 8
coins.
Solution Implementation
1from typing import List
2from functools import lru_cache
3from math import inf
4
5class Solution:
6 def maximumAmount(self, coins: List[List[int]]) -> int:
7 # Use Python's built-in LRU cache for memoization
8 @lru_cache(None)
9 def dfs(row: int, col: int, negatives_remaining: int) -> int:
10 # Base bounds condition: If out of matrix bounds, return negative infinity
11 if row >= m or col >= n:
12 return -inf
13
14 # Reached bottom-right cell: decide on remaining negatives allowance
15 if row == m - 1 and col == n - 1:
16 return max(coins[row][col], 0) if negatives_remaining else coins[row][col]
17
18 # Calculate the maximum amount of coins by moving to the right or down cells
19 max_coins = coins[row][col] + max(dfs(row + 1, col, negatives_remaining),
20 dfs(row, col + 1, negatives_remaining))
21
22 # If current cell has negative coins and negatives_remaining is positive
23 if coins[row][col] < 0 and negatives_remaining:
24 # Consider skipping the negative cell and move right or down with one less allowance
25 max_coins = max(max_coins,
26 dfs(row + 1, col, negatives_remaining - 1),
27 dfs(row, col + 1, negatives_remaining - 1))
28
29 return max_coins
30
31 # Initialize matrix dimensions
32 m, n = len(coins), len(coins[0])
33
34 # Start DFS from the top-left cell with the ability to skip up to 2 negatives
35 return dfs(0, 0, 2)
36
1class Solution {
2 private Integer[][][] memoization; // Memoization array to store results
3 private int[][] coinsGrid; // 2D array to store coin values
4 private int rows; // Number of rows in the grid
5 private int cols; // Number of columns in the grid
6
7 public int maximumAmount(int[][] coins) {
8 rows = coins.length; // Initialize the number of rows
9 cols = coins[0].length; // Initialize the number of columns
10 coinsGrid = coins; // Reference the coins grid
11 memoization = new Integer[rows][cols][3]; // Initialize the memoization array
12 return dfs(0, 0, 2); // Start DFS traversal from top-left cell with 2 opportunities to ignore a negative value
13 }
14
15 private int dfs(int i, int j, int opportunities) {
16 // Boundary condition: if out of bounds, return a large negative value to prevent invalid paths
17 if (i >= rows || j >= cols) {
18 return Integer.MIN_VALUE / 2;
19 }
20 // Return precomputed value if we have already calculated this state
21 if (memoization[i][j][opportunities] != null) {
22 return memoization[i][j][opportunities];
23 }
24 // Base condition: if we are at the bottom-right cell
25 if (i == rows - 1 && j == cols - 1) {
26 // Return the value of that cell, considering remaining opportunities to ignore negatives
27 return opportunities > 0 ? Math.max(0, coinsGrid[i][j]) : coinsGrid[i][j];
28 }
29
30 // Regular pathing: Move down or right, and pick the maximum value path
31 int maxValue = coinsGrid[i][j] + Math.max(dfs(i + 1, j, opportunities), dfs(i, j + 1, opportunities));
32
33 // Additional decision: Ignore a negative value if opportunities are available
34 if (coinsGrid[i][j] < 0 && opportunities > 0) {
35 // Consider ignoring the negative value and executing the available path
36 maxValue = Math.max(maxValue, Math.max(dfs(i + 1, j, opportunities - 1), dfs(i, j + 1, opportunities - 1)));
37 }
38
39 // Save the computed result in memoization and return it
40 return memoization[i][j][opportunities] = maxValue;
41 }
42}
43
1#include <vector>
2#include <algorithm>
3#include <climits>
4
5class Solution {
6public:
7 int maximumAmount(std::vector<std::vector<int>>& coins) {
8 int m = coins.size();
9 int n = coins[0].size();
10
11 // Create a 3D DP table initialized to -1
12 std::vector<std::vector<std::vector<int>>> dp(m, std::vector<std::vector<int>>(n, std::vector<int>(3, -1)));
13
14 // Recursive function (using lambda) to calculate the maximum amount
15 auto dfs = [&](auto&& dfs, int row, int col, int k) -> int {
16 // If out of bounds, return a large negative value
17 if (row >= m || col >= n) {
18 return INT_MIN / 2;
19 }
20
21 // Check if already computed
22 if (dp[row][col][k] != -1) {
23 return dp[row][col][k];
24 }
25
26 // If we are at the last cell
27 if (row == m - 1 && col == n - 1) {
28 return k > 0 ? std::max(0, coins[row][col]) : coins[row][col];
29 }
30
31 // Calculate the max amount if we consider the current cell
32 int result = coins[row][col] + std::max(dfs(dfs, row + 1, col, k), dfs(dfs, row, col + 1, k));
33
34 // Consider the case when we can negate this cell's value
35 if (coins[row][col] < 0 && k > 0) {
36 result = std::max({result, dfs(dfs, row + 1, col, k - 1), dfs(dfs, row, col + 1, k - 1)});
37 }
38
39 // Store the result in dp table and return
40 return dp[row][col][k] = result;
41 };
42
43 // Start the search from the top-left corner with k as 2 negations allowed
44 return dfs(dfs, 0, 0, 2);
45 }
46};
47
1// Function to calculate the maximum amount of coins that can be collected
2// Parameters:
3// - coins: A 2D array representing the grid of coins with possible negative values
4// Returns:
5// - Maximum coins that can be collected
6function maximumAmount(coins: number[][]): number {
7 // Destructure dimensions of the grid
8 const [m, n] = [coins.length, coins[0].length];
9
10 // Initialize a 3D array for memoization to store intermediate results
11 const memo = Array.from({ length: m }, () =>
12 Array.from({ length: n }, () => Array(3).fill(-Infinity)),
13 );
14
15 // Depth First Search function with memoization
16 // Parameters:
17 // - i, j: Current position in the grid
18 // - k: Number of negative coins we are allowed to skip
19 // Returns:
20 // - Maximum amount from position (i, j) onward with k skips available
21 const dfs = (i: number, j: number, k: number): number => {
22 // If out of bounds, return negative infinity
23 if (i >= m || j >= n) {
24 return -Infinity;
25 }
26
27 // Return precomputed value from memo array
28 if (memo[i][j][k] !== -Infinity) {
29 return memo[i][j][k];
30 }
31
32 // Base case: If we are at the last cell
33 if (i === m - 1 && j === n - 1) {
34 // If skips are available, consider max of 0 or current value
35 return k > 0 ? Math.max(0, coins[i][j]) : coins[i][j];
36 }
37
38 // Collect current coin and take the maximum from either the next row or column
39 let maxCoins = coins[i][j] + Math.max(dfs(i + 1, j, k), dfs(i, j + 1, k));
40
41 // If current coin is negative and skips are available
42 if (coins[i][j] < 0 && k > 0) {
43 // Consider skipping current coin and moving to the next row or column
44 maxCoins = Math.max(maxCoins, dfs(i + 1, j, k - 1), dfs(i, j + 1, k - 1));
45 }
46
47 // Store result in memo array and return
48 return (memo[i][j][k] = maxCoins);
49 };
50
51 // Start DFS from the top-left corner with two skips available
52 return dfs(0, 0, 2);
53}
54
Time and Space Complexity
The time complexity of the code is O(m * n * k)
, where m
is the number of rows and n
is the number of columns in the coins
array, and k
is the number of conversion opportunities, which is 2
in this problem.
The space complexity of the code is also O(m * n * k)
due to the memoization used in the depth-first search, storing results for each state (i, j, k)
.
Learn more about how to find time and space complexity quickly.
A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".
What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?
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 algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!