Cherry Pickup II
HardPrompt
You are given a rows x cols matrix grid representing a field of cherries where grid[i][j] represents the number of cherries that you can collect from the (i, j) cell.
You have two robots that can collect cherries for you:
- Robot #1 is located at the top-left corner
(0, 0), and - Robot #2 is located at the top-right corner
(0, cols - 1).
Return the maximum number of cherries collection using both robots by following the rules below:
- From a cell
(i, j), robots can move to cell(i + 1, j - 1),(i + 1, j), or(i + 1, j + 1). - When any robot passes through a cell, It picks up all cherries, and the cell becomes an empty cell.
- When both robots stay in the same cell, only one takes the cherries.
- Both robots cannot move outside of the grid at any moment.
- Both robots should reach the bottom row in
grid.
Example 1:
Input: grid = [[3,1,1],[2,5,1],[1,5,5],[2,1,1]]
Output: 24
Explanation: Path of robot #1 and #2 are described in color green and blue respectively.
Cherries taken by Robot #1, (3 + 2 + 5 + 2) = 12.
Cherries taken by Robot #2, (1 + 5 + 5 + 1) = 12.
Total of cherries: 12 + 12 = 24.Example 2:
Input: grid = [[1,0,0,0,0,0,1],[2,0,0,0,0,3,0],[2,0,9,0,0,0,0],[0,3,0,5,4,0,0],[1,0,2,3,0,0,6]]
Output: 28
Explanation: Path of robot #1 and #2 are described in color green and blue respectively.
Cherries taken by Robot #1, (1 + 9 + 5 + 2) = 17.
Cherries taken by Robot #2, (1 + 3 + 4 + 3) = 11.
Total of cherries: 17 + 11 = 28.
Constraints:
rows == grid.lengthcols == grid[i].length2 <= rows, cols <= 700 <= grid[i][j] <= 100
Approaches
3 approaches with complexity analysis and trade-offs.
This approach directly translates the problem's state transitions into a recursive function. The state is defined by the current row and the column positions of the two robots. Since both robots move down one row at a time, they are always in the same row.
Algorithm
- Define a recursive function
solve(row, col1, col2)that returns the maximum cherries collected from the currentrowdownwards, with robots at(row, col1)and(row, col2). - Base Case: If
rowis the last row, return the cherries at the robots' positions. Ifcol1orcol2are out of bounds, return a very small number to signify an invalid path. - Recursive Step: For the current state
(row, col1, col2), calculate the cherries collected at this step. Then, explore all 9 possible next positions (col1 + d1,col2 + d2whered1, d2are in{-1, 0, 1}) in the next row by making recursive calls. - The result for the current state is the sum of current cherries and the maximum value returned from the 9 recursive calls.
- The initial call is
solve(0, 0, cols - 1).
Walkthrough
We define a recursive function, say solve(row, col1, col2), which calculates the maximum cherries that can be collected from the current row to the bottom, given robot 1 is at (row, col1) and robot 2 is at (row, col2). The function works as follows:
- Base Case: When the robots reach the last row (
row == rows - 1), they collect the cherries in their respective cells and the recursion stops. The value returned isgrid[row][col1] + grid[row][col2](or justgrid[row][col1]if they are in the same cell). - Recursive Step: For any other row, the function calculates the cherries collected at the current cells. Then, it explores all possible next moves for both robots. Robot 1 can move to
col1-1,col1, orcol1+1in the next row, and similarly for robot 2. This gives3 * 3 = 9possible combinations of next positions. The function recursively calls itself for each of these 9 combinations and takes the maximum result. This maximum is added to the cherries collected at the current row. - Boundary Checks: The function must handle cases where a robot moves outside the grid boundaries. Such paths are invalid and should return a value that ensures they are not chosen (e.g., a very small negative number).
- The initial call to the function is
solve(0, 0, cols - 1).
class Solution { public int cherryPickup(int[][] grid) { int rows = grid.length; int cols = grid[0].length; return solve(0, 0, cols - 1, grid); } private int solve(int row, int col1, int col2, int[][] grid) { int rows = grid.length; int cols = grid[0].length; // Boundary checks for columns if (col1 < 0 || col1 >= cols || col2 < 0 || col2 >= cols) { return Integer.MIN_VALUE; } // Cherries at the current step int cherries = grid[row][col1]; if (col1 != col2) { cherries += grid[row][col2]; } // Base case: last row if (row == rows - 1) { return cherries; } // Recursive step: explore all 9 possible next moves int maxFutureCherries = 0; for (int dc1 = -1; dc1 <= 1; dc1++) { for (int dc2 = -1; dc2 <= 1; dc2++) { maxFutureCherries = Math.max(maxFutureCherries, solve(row + 1, col1 + dc1, col2 + dc2, grid)); } } return cherries + maxFutureCherries; }}Complexity
Time
O(9^rows). At each row, the function branches into 9 recursive calls. The depth of the recursion is `rows`, leading to an exponential number of calls.
Space
O(rows) for the recursion call stack.
Trade-offs
Pros
Simple to understand and implement as it directly models the problem statement.
Cons
Extremely inefficient due to a massive number of redundant computations for the same subproblems.
Will result in a 'Time Limit Exceeded' error on any non-trivial input size.
The brute-force recursive approach suffers from re-calculating the same subproblems multiple times. A subproblem is defined by the state (row, col1, col2). We can optimize this by storing the result of each subproblem in a memoization table (a 3D array) the first time it's computed. Subsequent calls for the same state will simply return the stored result.
Algorithm
- Create a 3D memoization array
memo[rows][cols][cols]and initialize it with a value like -1 to indicate uncomputed states. - Use the same recursive function
solve(row, col1, col2)as in the brute-force approach. - At the beginning of the function, check if
memo[row][col1][col2]is already computed (i.e., not -1). If so, return the stored value immediately. - If the state is not memoized, compute the result as before.
- Before returning the newly computed result, store it in
memo[row][col1][col2]to avoid re-computation in the future.
Walkthrough
This approach enhances the brute-force recursion by adding a cache, or memoization table, to store the results of subproblems. This is a top-down dynamic programming technique.
- We use a 3D array,
memo[rows][cols][cols], to store the results of our recursive functionsolve(row, col1, col2). - The
memoarray is initialized with a special value (e.g.,nullor -1) to indicate that a state has not yet been computed. - Inside the recursive function, before any computation, we check if
memo[row][col1][col2]already contains a valid result. If it does, we return it immediately. - If the result is not in the memo table, we compute it as in the brute-force approach.
- Before returning the computed result, we store it in
memo[row][col1][col2]for future use. - This technique ensures that each unique state
(row, col1, col2)is computed only once, dramatically improving performance.
class Solution { public int cherryPickup(int[][] grid) { int rows = grid.length; int cols = grid[0].length; Integer[][][] memo = new Integer[rows][cols][cols]; return solve(0, 0, cols - 1, grid, memo); } private int solve(int row, int col1, int col2, int[][] grid, Integer[][][] memo) { int rows = grid.length; int cols = grid[0].length; if (col1 < 0 || col1 >= cols || col2 < 0 || col2 >= cols) { return Integer.MIN_VALUE; } if (memo[row][col1][col2] != null) { return memo[row][col1][col2]; } int cherries = grid[row][col1]; if (col1 != col2) { cherries += grid[row][col2]; } if (row == rows - 1) { return cherries; } int maxFutureCherries = 0; for (int dc1 = -1; dc1 <= 1; dc1++) { for (int dc2 = -1; dc2 <= 1; dc2++) { maxFutureCherries = Math.max(maxFutureCherries, solve(row + 1, col1 + dc1, col2 + dc2, grid, memo)); } } memo[row][col1][col2] = cherries + maxFutureCherries; return memo[row][col1][col2]; }}Complexity
Time
O(rows * cols * cols). Each state `(row, col1, col2)` is computed exactly once. The computation for each state involves a constant number of operations (a loop of 9).
Space
O(rows * cols * cols). This is dominated by the space required for the memoization table. The recursion stack depth adds O(rows).
Trade-offs
Pros
Drastically more efficient than brute-force and guaranteed to solve the problem within time limits.
Often more intuitive to write than the bottom-up tabulation approach.
Cons
Uses significant space for the 3D memoization table.
Recursion overhead can make it slightly slower than an equivalent iterative (tabulation) solution.
This is the most efficient approach. It builds the solution iteratively from the bottom up, which avoids recursion overhead. Furthermore, it optimizes the space complexity by recognizing that to compute the maximum cherries for a given row, we only need the results from the immediately following row.
Algorithm
- Create a 2D DP array
dp[cols][cols]. - Initialization: Populate
dpwith the results for the last row (rows - 1).dp[c1][c2]will be the sum of cherries at(rows-1, c1)and(rows-1, c2). - Iteration: Loop
rowfromrows - 2down to0.- Inside the loop, create a new 2D array
current_dp[cols][cols]. - For each state
(row, c1, c2), calculate the maximum cherries from the next row by looking up the 9 possible next states in thedptable (which holds values forrow + 1). current_dp[c1][c2] = (cherries at current cells) + (max value from dp).- After the inner loops for
c1andc2are done, replace the olddptable withcurrent_dp(dp = current_dp).
- Inside the loop, create a new 2D array
- Result: After the main loop finishes, the answer is
dp[0][cols - 1].
Walkthrough
This bottom-up DP approach, also known as tabulation, iteratively builds the solution. We can optimize space because the calculation for any row r only depends on the results from row r+1.
- We use a 2D table,
dpof sizecols x cols, to store the maximum cherries that can be collected from a given row to the end. - Initialization: We start by populating
dpas if it represents the last row (rows - 1). For each pair of columns(c1, c2),dp[c1][c2]is initialized withgrid[rows-1][c1] + grid[rows-1][c2](or justgrid[rows-1][c1]ifc1 == c2). - Iteration: We then iterate backward from the second to last row (
r = rows - 2) up to the first row (r = 0). In each iteration, we compute acurrent_dptable for rowr. For each pair of columns(c1, c2), we find the maximum possible sum from the next row (r+1) by checking all 9 states reachable from(c1, c2). The values for rowr+1are already stored in ourdptable. The resultcurrent_dp[c1][c2]is the sum of cherries at(r, c1)and(r, c2)plus this maximum value from the next row. - After computing the
current_dptable for rowr, we updatedp = current_dpto be used for the calculation of the previous row (r-1). - Final Result: After the main loop completes,
dpholds the results for row 0. The answer isdp[0][cols - 1], corresponding to the robots' initial positions.
class Solution { public int cherryPickup(int[][] grid) { int rows = grid.length; int cols = grid[0].length; int[][] dp = new int[cols][cols]; // Initialize for the last row for (int c1 = 0; c1 < cols; c1++) { for (int c2 = 0; c2 < cols; c2++) { if (c1 == c2) { dp[c1][c2] = grid[rows - 1][c1]; } else { dp[c1][c2] = grid[rows - 1][c1] + grid[rows - 1][c2]; } } } // Iterate from the second to last row up to the first for (int r = rows - 2; r >= 0; r--) { int[][] current_dp = new int[cols][cols]; for (int c1 = 0; c1 < cols; c1++) { for (int c2 = 0; c2 < cols; c2++) { int maxNextRow = 0; // Find max cherries from the next row for (int dc1 = -1; dc1 <= 1; dc1++) { for (int dc2 = -1; dc2 <= 1; dc2++) { int nc1 = c1 + dc1; int nc2 = c2 + dc2; if (nc1 >= 0 && nc1 < cols && nc2 >= 0 && nc2 < cols) { maxNextRow = Math.max(maxNextRow, dp[nc1][nc2]); } } } int currentCherries = grid[r][c1]; if (c1 != c2) { currentCherries += grid[r][c2]; } current_dp[c1][c2] = currentCherries + maxNextRow; } } dp = current_dp; } return dp[0][cols - 1]; }}Complexity
Time
O(rows * cols * cols). We have a loop for rows, and nested loops for `c1`, `c2`, and the 9 possible moves.
Space
O(cols * cols). We only need space for two 2D arrays of size `cols x cols` to store the DP states for the current and next rows.
Trade-offs
Pros
Most efficient in terms of both time and space.
Avoids recursion overhead, which can lead to a slight performance gain.
Uses significantly less memory than non-optimized DP approaches.
Cons
Can be less intuitive to formulate and implement compared to the top-down memoization approach.
Solutions
Solution
class Solution {public int cherryPickup(int[][] grid) { int m = grid.length, n = grid[0].length; int[][][] f = new int[m][n][n]; for (var g : f) { for (var h : g) { Arrays.fill(h, -1); } } f[0][0][n - 1] = grid[0][0] + grid[0][n - 1]; for (int i = 1; i < m; ++i) { for (int j1 = 0; j1 < n; ++j1) { for (int j2 = 0; j2 < n; ++j2) { int x = grid[i][j1] + (j1 == j2 ? 0 : grid[i][j2]); for (int y1 = j1 - 1; y1 <= j1 + 1; ++y1) { for (int y2 = j2 - 1; y2 <= j2 + 1; ++y2) { if (y1 >= 0 && y1 < n && y2 >= 0 && y2 < n && f[i - 1][y1][y2] != -1) { f[i][j1][j2] = Math.max(f[i][j1][j2], f[i - 1][y1][y2] + x); } } } } } } int ans = 0; for (int j1 = 0; j1 < n; ++j1) { for (int j2 = 0; j2 < n; ++j2) { ans = Math.max(ans, f[m - 1][j1][j2]); } } return ans; }}class Solution {public: int cherryPickup(vector<vector<int>> &grid) { int m = grid.size(), n = grid[0].size(); int f[m][n][n]; memset(f, -1, sizeof(f)); f[0][0][n - 1] = grid[0][0] + grid[0][n - 1]; for (int i = 1; i < m; ++i) { for (int j1 = 0; j1 < n; ++j1) { for (int j2 = 0; j2 < n; ++j2) { int x = grid[i][j1] + (j1 == j2 ? 0 : grid[i][j2]); for (int y1 = j1 - 1; y1 <= j1 + 1; ++y1) { for (int y2 = j2 - 1; y2 <= j2 + 1; ++y2) { if (y1 >= 0 && y1 < n && y2 >= 0 && y2 < n && f[i - 1][y1][y2] != -1) { f[i][j1][j2] = max(f[i][j1][j2], f[i - 1][y1][y2] + x); } } } } } } int ans = 0; for (int j1 = 0; j1 < n; ++j1) { for (int j2 = 0; j2 < n; ++j2) { ans = max(ans, f[m - 1][j1][j2]); } } return ans; }};class Solution: def cherryPickup(self, grid: List[List[int]]) -> int: m, n = len(grid), len(grid[0]) f = [[[- 1] * n for _ in range(n)] for _ in range(m)] f[0][0][n - 1] = grid[0][0] + grid[0][n - 1] for i in range(1, m): for j1 in range(n): for j2 in range(n): x = grid[i][j1] + (0 if j1 == j2 else grid[i][j2]) for y1 in range(j1 - 1, j1 + 2): for y2 in range(j2 - 1, j2 + 2): if 0 <= y1 < n and 0 <= y2 < n and f[i - 1][y1][y2] != - 1: f[i][j1][j2] = max(f[i][j1][j2], f[i - 1][y1][y2] + x) return max(f[- 1][j1][j2] for j1, j2 in product(range(n), range(n)))Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.