1463. Cherry Pickup II - Explanation

Problem Link



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Dynamic Programming - Using memoization or tabulation to solve optimization problems with overlapping subproblems
  • 2D/3D Arrays - Working with multi-dimensional data structures for state representation
  • Recursion with Multiple Variables - Tracking multiple entities (two robots) simultaneously through recursive state exploration

1. Recursion

Intuition

Two robots start at the top corners of the grid and move down simultaneously, one row at a time. At each row, each robot can move diagonally left, straight down, or diagonally right. We track both robot positions and explore all 9 combinations of moves (3 choices for robot 1 times 3 choices for robot 2). If both robots land on the same cell, we only count the cherries once. We enforce c1 <= c2 to avoid duplicate states since the robots are interchangeable.

Algorithm

  1. Define a recursive function dfs(r, c1, c2) where r is the current row, c1 is robot 1's column, and c2 is robot 2's column.
  2. Base case: If c1 or c2 is out of bounds, or c1 > c2, return 0.
  3. Base case: If r equals ROWS - 1, return the sum of cherries at both positions (counting once if they overlap).
  4. For each of the 9 direction combinations (c1_d, c2_d in [-1, 0, 1]):
    • Recursively call dfs(r + 1, c1 + c1_d, c2 + c2_d).
    • Track the maximum result.
  5. Add cherries from current positions to the result and return.
class Solution:
    def cherryPickup(self, grid: List[List[int]]) -> int:
        ROWS, COLS = len(grid), len(grid[0])

        def dfs(r, c1, c2):
            if c1 < 0 or c2 < 0 or c1 >= COLS or c2 >= COLS or c1 > c2:
                return 0
            if r == ROWS - 1:
                return grid[r][c1] + (grid[r][c2] if c1 != c2 else 0)

            res = 0
            for c1_d in [-1, 0, 1]:
                for c2_d in [-1, 0, 1]:
                    res = max(res, dfs(r + 1, c1 + c1_d, c2 + c2_d))

            return res + grid[r][c1] + (grid[r][c2] if c1 != c2 else 0)

        return dfs(0, 0, COLS - 1)

Time & Space Complexity

  • Time complexity: O(m9n)O(m * 9 ^ n)
  • Space complexity: O(n)O(n) for recursion stack.

Where nn is the number of rows and mm is the number of columns in the grid.


2. Dynamic Programming (Top-Down)

Intuition

The recursive solution recomputes the same states multiple times. Since the state is defined by (r, c1, c2), we can use a 3D cache to store results for previously visited states. When we encounter a state we have already solved, we return the cached value instead of recomputing it.

Algorithm

  1. Create a 3D cache array of size [ROWS][COLS][COLS] initialized to -1.
  2. In the recursive function, first check if the state is already in the cache. If so, return the cached value.
  3. Perform the same logic as the recursive solution: handle base cases, try all 9 move combinations, and compute the maximum.
  4. Before returning, store the computed result in the cache.
  5. Return the cached result.
class Solution:
    def cherryPickup(self, grid: List[List[int]]) -> int:
        ROWS, COLS = len(grid), len(grid[0])
        cache = {}

        def dfs(r, c1, c2):
            if (r, c1, c2) in cache:
                return cache[(r, c1, c2)]
            if c1 == c2 or min(c1, c2) < 0 or max(c1, c2) >= COLS:
                return 0
            if r == ROWS - 1:
                return grid[r][c1] + grid[r][c2]

            res = 0
            for c1_d in [-1, 0, 1]:
                for c2_d in [-1, 0, 1]:
                    res = max(res, dfs(r + 1, c1 + c1_d, c2 + c2_d))

            cache[(r, c1, c2)] = res + grid[r][c1] + (grid[r][c2] if c1 != c2 else 0)
            return cache[(r, c1, c2)]

        return dfs(0, 0, COLS - 1)

Time & Space Complexity

  • Time complexity: O(nm2)O(n * m ^ 2)
  • Space complexity: O(nm2)O(n * m ^ 2)

Where nn is the number of rows and mm is the number of columns in the grid.


3. Dynamic Programming (Bottom-Up)

Intuition

Instead of using recursion with memoization, we can build the solution iteratively from the bottom row up. For each cell combination at row r, we look at all possible transitions from row r+1 and take the maximum. This eliminates recursion overhead and makes the computation order explicit.

Algorithm

  1. Create a 3D DP array of size [ROWS][COLS][COLS].
  2. Process rows from bottom to top (r = ROWS-1 to 0).
  3. For each (c1, c2) pair at row r:
    • Calculate cherries at current positions (handling overlap).
    • If at the last row, the value is just the current cherries.
    • Otherwise, look at all 9 possible (nc1, nc2) positions in row r+1.
    • Take the maximum from valid next positions and add current cherries.
  4. Store the result in dp[r][c1][c2].
  5. Return dp[0][0][COLS-1].
class Solution:
    def cherryPickup(self, grid: List[List[int]]) -> int:
        ROWS, COLS = len(grid), len(grid[0])
        dp = [[[0] * COLS for _ in range(COLS)] for _ in range(ROWS)]

        for r in range(ROWS - 1, -1, -1):
            for c1 in range(COLS):
                for c2 in range(COLS):
                    res = grid[r][c1]
                    if c1 != c2:
                        res += grid[r][c2]

                    if r != ROWS - 1:
                        max_cherries = 0
                        for d1 in [-1, 0, 1]:
                            for d2 in [-1, 0, 1]:
                                nc1, nc2 = c1 + d1, c2 + d2
                                if 0 <= nc1 < COLS and 0 <= nc2 < COLS:
                                    max_cherries = max(max_cherries, dp[r + 1][nc1][nc2])
                        res += max_cherries

                    dp[r][c1][c2] = res

        return dp[0][0][COLS - 1]

Time & Space Complexity

  • Time complexity: O(nm2)O(n * m ^ 2)
  • Space complexity: O(nm2)O(n * m ^ 2)

Where nn is the number of rows and mm is the number of columns in the grid.


4. Dynamic Programming (Space Optimized)

Intuition

Since each row's computation only depends on the row below it, we do not need to store the entire 3D array. We can use two 2D arrays: one for the current row and one for the previous row (the row below). After processing each row, we swap them. This reduces space complexity from O(n * m^2) to O(m^2).

Algorithm

  1. Create a 2D dp array of size [COLS][COLS] for the previous row's results.
  2. Process rows from bottom to top.
  3. For each row, create a new 2D cur_dp array for the current row.
  4. For each (c1, c2) pair where c1 <= c2:
    • Calculate cherries at current positions.
    • Look at all 9 next positions in the dp array (previous row).
    • Take the maximum and add current cherries.
  5. After processing the row, set dp = cur_dp.
  6. Return dp[0][COLS-1].
class Solution:
    def cherryPickup(self, grid: List[List[int]]) -> int:
        ROWS, COLS = len(grid), len(grid[0])
        dp = [[0] * COLS for _ in range(COLS)]

        for r in reversed(range(ROWS)):
            cur_dp = [[0] * COLS for _ in range(COLS)]
            for c1 in range(COLS):
                for c2 in range(c1, COLS):
                    max_cherries = 0
                    cherries = grid[r][c1] + (grid[r][c2] if c1 != c2 else 0)
                    for d1 in [-1, 0, 1]:
                        for d2 in [-1, 0, 1]:
                            nc1, nc2 = c1 + d1, c2 + d2
                            if 0 <= nc1 < COLS and 0 <= nc2 < COLS:
                                max_cherries = max(max_cherries, cherries + dp[nc1][nc2])
                    cur_dp[c1][c2] = max_cherries
            dp = cur_dp

        return dp[0][COLS - 1]

Time & Space Complexity

  • Time complexity: O(nm2)O(n * m ^ 2)
  • Space complexity: O(m2)O(m ^ 2)

Where nn is the number of rows and mm is the number of columns in the grid.


Common Pitfalls

Double Counting Cherries When Robots Overlap

Forgetting to check if both robots land on the same cell and counting cherries twice. When c1 == c2, you must only add grid[r][c1] once, not grid[r][c1] + grid[r][c2].

# Wrong: always adds both
res = grid[r][c1] + grid[r][c2]

# Correct: check for overlap
res = grid[r][c1] + (grid[r][c2] if c1 != c2 else 0)

Using 4 Directions Instead of 3

Treating this like a general grid traversal where robots can move in any direction. Each robot can only move down-left, down, or down-right (3 choices), not up or stay in place. This gives 9 total combinations per step, not 16.

Incorrect State Space for Memoization

Using only (r, c1) or (r, c2) for caching, forgetting that the state depends on both robot positions simultaneously. The correct state is (r, c1, c2) since both robots move together row by row.

Off-by-One Error on Base Case

Checking r == ROWS instead of r == ROWS - 1 for the base case. Since indexing is 0-based, the last row is at index ROWS - 1, and that is where you should collect final cherries and stop recursion.