Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

741. Cherry Pickup - Leetcode Solution

Problem Description

The Cherry Pickup problem involves a square grid represented as a 2D array called grid, where each cell may contain:

  • 0: an empty cell,
  • 1: a cell with a cherry,
  • -1: a cell with a thorn (impassable).

The goal is to collect the maximum number of cherries by making two trips:
  1. Start at the top-left corner (0,0) and move to the bottom-right corner (N-1,N-1), moving only right or down at each step.
  2. Return from (N-1,N-1) back to (0,0), moving only left or up at each step.

On both trips, you can only pass through cells that are not thorns (-1). If a cell contains a cherry, you pick it up (the cell becomes empty after picking up). You cannot pick up the same cherry twice. If there is no valid path for either trip, you should return 0.

Key Constraints:
  • Each cherry can be collected only once.
  • You must not pass through thorns.
  • There is only one valid solution for the maximum cherries you can collect.

Thought Process

At first glance, the problem seems to ask for two separate traversals: one forward and one backward, each maximizing the cherries collected. A brute-force approach would try all possible paths for both trips, but this quickly becomes infeasible due to the exponential number of possible routes.

A key insight is that the two trips can be modeled as two people starting at (0,0) and both moving to (N-1,N-1) at the same time, each taking a step either down or right. This is because the return trip is simply a reverse traversal, and collecting cherries on the way back is equivalent to collecting them in a second simultaneous path. This reframing allows us to use dynamic programming to efficiently keep track of the maximum cherries collected, making sure not to double-count cherries picked up by both "travellers" if they land on the same cell.

The challenge then becomes designing a dynamic programming state that efficiently tracks both positions and avoids redundant calculations.

Solution Approach

To solve this problem efficiently, we use dynamic programming (DP) to simulate two people moving from the top-left to the bottom-right of the grid at the same time.

Step-by-step Algorithm:

  1. Model the Two Trips as Two Travellers:
    • Instead of going forward and then backward, imagine two people starting at (0,0) and moving to (N-1,N-1) simultaneously.
    • At each step, both can move either right or down.
  2. Dynamic Programming State:
    • Let dp[r1][c1][r2] represent the maximum cherries collected when:
      • Person 1 is at (r1, c1)
      • Person 2 is at (r2, c2), where c2 = r1 + c1 - r2 (since both have taken the same number of steps)
    • This reduces the state space and avoids redundant calculations.
  3. Transition:
    • At each step, both persons can move right or down, leading to four possible combinations.
    • If either person steps on a thorn or goes out of bounds, that path is invalid.
    • If both are on the same cell, only count the cherry once.
  4. Memoization:
    • Store intermediate results to avoid recalculating the same state.
  5. Base Case:
    • If both reach (N-1, N-1), return the value in that cell (if not a thorn).
  6. Return the Maximum:
    • Return the maximum cherries collected from all possible moves, or 0 if no valid path exists.

Why this works:
  • By simulating both trips as two simultaneous traversals, we account for cherry collection and avoid double-counting.
  • Memoization ensures we only compute each state once, making the solution efficient even for larger grids.

Example Walkthrough

Sample Input:

    grid = [
      [0, 1, -1],
      [1, 0, -1],
      [1, 1,  1]
    ]
    

Step-by-step:
  1. Both persons start at (0,0) (no cherry).
  2. Possible moves: right or down. If both go right, one will hit a thorn at (0,2); so, only safe moves are down or right to non-thorn cells.
  3. As they progress, they collect cherries at (0,1), (1,0), (2,0), (2,1), and (2,2).
  4. The DP ensures that if both land on the same cherry, it is counted only once.
  5. By considering all valid paths and maximizing the cherry count, the solution finds the optimal collection (which is 5 in this case).

Why this works: The DP explores all possible movement combinations, prunes invalid paths, and always tracks the maximum cherries collected.

Time and Space Complexity

Brute-force Approach:

  • Time Complexity: Exponential, as each step has up to 2 choices for each of 2 trips, leading to O(4^{2N}) paths.
  • Space Complexity: Exponential, due to recursive call stack and path tracking.

Optimized DP Approach:
  • Time Complexity: O(N^3), as the DP state is defined by three variables (r1, c1, r2), each ranging from 0 to N-1.
  • Space Complexity: O(N^3), for memoization storage of all possible states.

Why: The DP state space is cubic in N, making it feasible for reasonable grid sizes (typically N <= 50).

Summary

The Cherry Pickup problem can be efficiently solved by modeling both trips as two synchronized journeys from the top-left to the bottom-right of the grid. Using a dynamic programming approach with memoization, we systematically explore all valid paths, maximize cherry collection, and avoid double-counting. The cubic state space ensures efficiency, and the solution elegantly handles the challenge of collecting each cherry only once.

Code Implementation

from functools import lru_cache

class Solution:
    def cherryPickup(self, grid):
        N = len(grid)
        
        @lru_cache(None)
        def dp(r1, c1, r2):
            c2 = r1 + c1 - r2
            if (r1 >= N or c1 >= N or r2 >= N or c2 >= N or
                grid[r1][c1] == -1 or grid[r2][c2] == -1):
                return float('-inf')
            if r1 == c1 == N-1:
                return grid[r1][c1]
            if r2 == c2 == N-1:
                return grid[r2][c2]
            
            cherries = grid[r1][c1]
            if (r1, c1) != (r2, c2):
                cherries += grid[r2][c2]
            next_best = max(
                dp(r1+1, c1, r2+1),
                dp(r1, c1+1, r2),
                dp(r1+1, c1, r2),
                dp(r1, c1+1, r2+1)
            )
            return cherries + next_best

        result = dp(0, 0, 0)
        return max(0, result)
      
class Solution {
public:
    int cherryPickup(vector<vector<int>>& grid) {
        int N = grid.size();
        vector<vector<vector<int>>> memo(N, vector<vector<int>>(N, vector<int>(N, INT_MIN)));
        function<int(int,int,int)> dp = [&](int r1, int c1, int r2) {
            int c2 = r1 + c1 - r2;
            if (r1 >= N || c1 >= N || r2 >= N || c2 >= N ||
                grid[r1][c1] == -1 || grid[r2][c2] == -1)
                return INT_MIN;
            if (r1 == N-1 && c1 == N-1)
                return grid[r1][c1];
            if (r2 == N-1 && c2 == N-1)
                return grid[r2][c2];
            if (memo[r1][c1][r2] != INT_MIN)
                return memo[r1][c1][r2];
            int cherries = grid[r1][c1];
            if (r1 != r2 || c1 != c2)
                cherries += grid[r2][c2];
            int next = max({
                dp(r1+1, c1, r2+1),
                dp(r1, c1+1, r2),
                dp(r1+1, c1, r2),
                dp(r1, c1+1, r2+1)
            });
            memo[r1][c1][r2] = cherries + next;
            return memo[r1][c1][r2];
        };
        int res = dp(0, 0, 0);
        return max(0, res);
    }
};
      
class Solution {
    public int cherryPickup(int[][] grid) {
        int N = grid.length;
        Integer[][][] memo = new Integer[N][N][N];

        return Math.max(0, dp(grid, 0, 0, 0, memo));
    }

    private int dp(int[][] grid, int r1, int c1, int r2, Integer[][][] memo) {
        int N = grid.length;
        int c2 = r1 + c1 - r2;
        if (r1 >= N || c1 >= N || r2 >= N || c2 >= N ||
            grid[r1][c1] == -1 || grid[r2][c2] == -1)
            return Integer.MIN_VALUE;
        if (r1 == N-1 && c1 == N-1)
            return grid[r1][c1];
        if (r2 == N-1 && c2 == N-1)
            return grid[r2][c2];
        if (memo[r1][c1][r2] != null)
            return memo[r1][c1][r2];
        int cherries = grid[r1][c1];
        if (r1 != r2 || c1 != c2)
            cherries += grid[r2][c2];
        int next = Math.max(
            Math.max(dp(grid, r1+1, c1, r2+1, memo), dp(grid, r1, c1+1, r2, memo)),
            Math.max(dp(grid, r1+1, c1, r2, memo), dp(grid, r1, c1+1, r2+1, memo))
        );
        memo[r1][c1][r2] = cherries + next;
        return memo[r1][c1][r2];
    }
}
      
var cherryPickup = function(grid) {
    const N = grid.length;
    const memo = new Map();

    function dp(r1, c1, r2) {
        let c2 = r1 + c1 - r2;
        let key = r1 + ',' + c1 + ',' + r2;
        if (r1 >= N || c1 >= N || r2 >= N || c2 >= N ||
            grid[r1][c1] === -1 || grid[r2][c2] === -1)
            return -Infinity;
        if (r1 === N-1 && c1 === N-1)
            return grid[r1][c1];
        if (r2 === N-1 && c2 === N-1)
            return grid[r2][c2];
        if (memo.has(key)) return memo.get(key);
        let cherries = grid[r1][c1];
        if (r1 !== r2 || c1 !== c2)
            cherries += grid[r2][c2];
        let next = Math.max(
            dp(r1+1, c1, r2+1),
            dp(r1, c1+1, r2),
            dp(r1+1, c1, r2),
            dp(r1, c1+1, r2+1)
        );
        memo.set(key, cherries + next);
        return cherries + next;
    }

    let res = dp(0,0,0);
    return Math.max(0, res);
};