Facebook Pixel

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 of coins[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 or j >= n, the robot is out of bounds, leading to an invalid path. Return a very small value to indicate this.

  • If i = m - 1 and j = n - 1, the robot reaches the grid's bottom-right corner. Consider conversion of the robber if possible. Return the maximum between coins[i][j] and 0 if conversions are left, else return coins[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.
  • 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:

  1. Define the Recursive Function: We define a function dfs(i, j, k) where i and j represent the current row and column in the grid, respectively, and k 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.

  2. Base Cases:

    • If the robot moves out of bounds (i >= m or j >= n), return a very small number (negative infinity) to signify an invalid path.
    • If the robot reaches the destination cell (m - 1, n - 1), return max(0, coins[i][j]) if neutralization opportunities are available (k > 0), else return coins[i][j].
  3. 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.
  4. 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 with k - 1 opportunities remaining.
  5. 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 Evaluator

Example 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.

  1. Start at (0, 0):

    • Begin at (0, 0) with 2 neutralization opportunities.
    • The cell (0, 0) has 0 coins, so we move onward.
  2. Move to (0, 1):

    • Choose to move right to (0, 1) which contains 1 coin.
    • Total coins are now 1.
  3. 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.
  4. Move to (1, 2):

    • From (0, 2), move down to (1, 2) gaining 2 coins.
    • Total coins become 3.
  5. Move to (2, 2):

    • Move to (2, 2), the destination, with 5 coins.
    • Total coins become 8.
    • The path is reached with one special ability unused (k = 1).

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.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

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

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


Load More