Prerequisites

Before attempting this problem, you should be comfortable with:

  • Dynamic Programming - Using memoization or tabulation to solve optimization problems with overlapping subproblems
  • Multi-dimensional DP - Managing state across 3 or 4 dimensions to track multiple path positions
  • Path Finding in Grids - Understanding movement constraints and handling obstacles in 2D matrices

1. Recursion

Intuition

Instead of thinking about one person going from the top-left to the bottom-right and then back, we can simulate two people starting from the top-left and moving simultaneously toward the bottom-right. Since both paths must eventually reach the destination, we track their positions using four variables (r1, c1) and (r2, c2). At each step, both move either down or right, giving us 4 combinations of moves. When they land on the same cell, we only count the cherries once to avoid double counting.

Algorithm

  1. Use a recursive function dfs(r1, c1, r2, c2) that returns the maximum cherries collected when person 1 is at (r1, c1) and person 2 is at (r2, c2).
  2. Base case: If either person goes out of bounds or lands on a thorn (-1), return a large negative value to invalidate this path.
  3. Base case: If both reach the destination (n-1, n-1), return the cherry value at that cell.
  4. Try all 4 combinations of moves: both down, both right, one down and one right, or vice versa.
  5. Add cherries from both current positions, but subtract one if they are on the same cell.
  6. Return the maximum result. If the final answer is negative, return 0 (no valid path exists).
class Solution:
    def cherryPickup(self, grid: List[List[int]]) -> int:
        n = len(grid)
        def dfs(r1, c1, r2, c2):
            if r1 >= n or c1 >= n or r2 >= n or c2 >= n or grid[r1][c1] == -1 or grid[r2][c2] == -1:
                return -1000

            if r1 == n - 1 and r2 == n - 1 and c1 == n - 1 and c2 == n - 1:
                return grid[r1][c1]

            res = dfs(r1 + 1, c1, r2 + 1, c2)
            res = max(res, dfs(r1 + 1, c1, r2, c2 + 1))
            res = max(res, dfs(r1, c1 + 1, r2 + 1, c2))
            res = max(res, dfs(r1, c1 + 1, r2, c2 + 1))
            res += grid[r1][c1] + grid[r2][c2]
            res -= (grid[r1][c1] if (r1 == r2 and c1 == c2) else 0)
            return res

        return max(0, dfs(0, 0, 0, 0))

Time & Space Complexity

  • Time complexity: O(16n)O(16 ^ n)
  • Space complexity: O(n)O(n) for recursion stack.

2. Dynamic Programming (Top-Down)

Intuition

The recursive solution has overlapping subproblems since the same combination of positions can be reached through different paths. By storing the results of each state (r1, c1, r2, c2) in a 4D memoization table, we avoid redundant calculations. This transforms the exponential time complexity into polynomial time.

Algorithm

  1. Create a 4D DP array initialized to negative infinity to track visited states.
  2. Use the same recursive function dfs(r1, c1, r2, c2) as before.
  3. Before computing, check if the current state is already cached in the DP array. If so, return the cached value.
  4. After computing the result for a state, store it in the DP array before returning.
  5. The rest of the logic remains identical to the recursive approach.
class Solution:
    def cherryPickup(self, grid: List[List[int]]) -> int:
        n = len(grid)
        dp = [[[[float("-inf")] * n for _ in range(n)] for _ in range(n)] for _ in range(n)]

        def dfs(r1, c1, r2, c2):
            if r1 >= n or c1 >= n or r2 >= n or c2 >= n or grid[r1][c1] == -1 or grid[r2][c2] == -1:
                return -1000

            if r1 == n - 1 and r2 == n - 1 and c1 == n - 1 and c2 == n - 1:
                return grid[r1][c1]

            if dp[r1][c1][r2][c2] != float("-inf"):
                return dp[r1][c1][r2][c2]

            res = dfs(r1 + 1, c1, r2 + 1, c2)
            res = max(res, dfs(r1 + 1, c1, r2, c2 + 1))
            res = max(res, dfs(r1, c1 + 1, r2 + 1, c2))
            res = max(res, dfs(r1, c1 + 1, r2, c2 + 1))
            res += grid[r1][c1] + grid[r2][c2]
            if r1 == r2 and c1 == c2:
                res -= grid[r1][c1]

            dp[r1][c1][r2][c2] = res
            return res

        return max(0, dfs(0, 0, 0, 0))

Time & Space Complexity

  • Time complexity: O(n4)O(n ^ 4)
  • Space complexity: O(n4)O(n ^ 4)

3. Dynamic Programming (Top-Down) Optimized

Intuition

We can reduce the state space by observing that both people always take the same number of steps. If person 1 is at (r1, c1), then they have taken r1 + c1 steps. Since person 2 has also taken the same number of steps, if we know r2, we can derive c2 as (r1 + c1 - r2). This reduces our state from 4 dimensions to 3 dimensions.

Algorithm

  1. Create a 3D DP array using only (r1, c1, r2) as state variables.
  2. Compute c2 dynamically as c2 = r1 + c1 - r2.
  3. Check bounds for all four coordinates, including the computed c2.
  4. Base case: When person 1 reaches (n-1, n-1), return the cherry value.
  5. Try all 4 move combinations, recursively compute the maximum cherries, and cache the result.
  6. Add cherries from both positions, avoiding double counting when positions match.
class Solution:
    def cherryPickup(self, grid: List[List[int]]) -> int:
        n = len(grid)
        dp = [[[float("-inf")] * n for _ in range(n)] for _ in range(n)]

        def dfs(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 -1000

            if r1 == n - 1 and c1 == n - 1:
                return grid[r1][c1]

            if dp[r1][c1][r2] != float("-inf"):
                return dp[r1][c1][r2]

            res = dfs(r1 + 1, c1, r2 + 1)
            res = max(res, dfs(r1 + 1, c1, r2))
            res = max(res, dfs(r1, c1 + 1, r2 + 1))
            res = max(res, dfs(r1, c1 + 1, r2))
            res += grid[r1][c1]
            if (r1, c1) != (r2, c2):
                res += grid[r2][c2]

            dp[r1][c1][r2] = res
            return res

        return max(0, dfs(0, 0, 0))

Time & Space Complexity

  • Time complexity: O(n3)O(n ^ 3)
  • Space complexity: O(n3)O(n ^ 3)

4. Dynamic Programming (Bottom-Up)

Intuition

Instead of recursion with memoization, we can fill the DP table iteratively starting from the destination and working backward to the start. We iterate through all valid combinations of (r1, c1, r2) in reverse order, computing c2 from the constraint, and build up the solution from smaller subproblems.

Algorithm

  1. Create a 3D DP array with dimensions [n][n][n].
  2. Iterate from (n-1, n-1, n-1) down to (0, 0, 0) for (r1, c1, r2).
  3. For each state, compute c2 = r1 + c1 - r2. Skip if c2 is out of bounds.
  4. Skip states where either position lands on a thorn.
  5. Base case: If at destination, store the cherry value.
  6. Otherwise, look at all 4 possible next states (from the already computed future states) and take the maximum.
  7. Add current cherries, avoiding double counting when both are on the same cell.
  8. Return dp[0][0][0], clamped to 0 if negative.
class Solution:
    def cherryPickup(self, grid: List[List[int]]) -> int:
        n = len(grid)
        dp = [[[float("-inf")] * n for _ in range(n)] for _ in range(n)]

        for r1 in reversed(range(n)):
            for c1 in reversed(range(n)):
                for r2 in reversed(range(n)):
                    c2 = r1 + c1 - r2
                    if c2 < 0 or c2 >= n:
                        continue
                    if grid[r1][c1] == -1 or grid[r2][c2] == -1:
                        continue

                    if r1 == n - 1 and c1 == n - 1:
                        dp[r1][c1][r2] = grid[r1][c1]
                    else:
                        res = max(
                            dp[r1 + 1][c1][r2 + 1] if r1 + 1 < n and r2 + 1 < n else -1000,
                            dp[r1 + 1][c1][r2] if r1 + 1 < n else -1000,
                            dp[r1][c1 + 1][r2 + 1] if c1 + 1 < n and r2 + 1 < n else -1000,
                            dp[r1][c1 + 1][r2] if c1 + 1 < n else -1000
                        )
                        if res == -1000:
                            continue
                        res += grid[r1][c1]
                        if (r1, c1) != (r2, c2):
                            res += grid[r2][c2]
                        dp[r1][c1][r2] = res

        return max(0, dp[0][0][0])

Time & Space Complexity

  • Time complexity: O(n3)O(n ^ 3)
  • Space complexity: O(n3)O(n ^ 3)

5. Dynamic Programming (Space Optimized)

Intuition

Since we process states by the total number of steps k = r1 + c1 = r2 + c2, and each state only depends on states from the previous step count, we only need to keep track of the previous layer. This reduces space from O(n^3) to O(n^2) by using two 2D arrays that alternate between the current and previous step.

Algorithm

  1. Create a 2D prev array of size [n][n] representing states for the previous step count.
  2. Initialize prev[0][0] with the starting cell's cherry value.
  3. For each step count k from 1 to 2n-2:
    • Create a new 2D dp array for the current step.
    • For each valid (r1, r2) pair where c1 = k - r1 and c2 = k - r2 are within bounds:
      • Look at all 4 transitions from the previous step.
      • Take the maximum and add current cherries (avoiding double counting).
    • Set prev = dp for the next iteration.
  4. Return prev[n-1][n-1], clamped to 0 if negative.
class Solution:
    def cherryPickup(self, grid: List[List[int]]) -> int:
        n = len(grid)
        prev = [[float("-inf")] * n for _ in range(n)]
        prev[0][0] = grid[0][0]

        for k in range(1, 2 * n - 1):
            dp = [[float("-inf")] * n for _ in range(n)]
            for r1 in range(max(0, k - (n - 1)), min(n, k + 1)):
                c1 = k - r1
                if c1 >= n or grid[r1][c1] == -1:
                    continue
                for r2 in range(max(0, k - (n - 1)), min(n, k + 1)):
                    c2 = k - r2
                    if c2 >= n or grid[r2][c2] == -1:
                        continue
                    val = prev[r1][r2]
                    if r1 > 0: val = max(val, prev[r1 - 1][r2])
                    if r2 > 0: val = max(val, prev[r1][r2 - 1])
                    if r1 > 0 and r2 > 0: val = max(val, prev[r1 - 1][r2 - 1])
                    if val < 0: continue
                    val += grid[r1][c1]
                    if r1 != r2: val += grid[r2][c2]
                    dp[r1][r2] = val
            prev = dp

        return max(0, prev[n - 1][n - 1])

Time & Space Complexity

  • Time complexity: O(n3)O(n ^ 3)
  • Space complexity: O(n2)O(n ^ 2)

Common Pitfalls

Solving as Two Independent Paths

Running a greedy or DP solution for the forward path, removing collected cherries, then solving the return path separately. This approach fails because the optimal combined solution may require a suboptimal forward path to enable a better return path. Both paths must be considered simultaneously.

Double Counting Cherries When Paths Overlap

Forgetting to subtract cherries when both simulated people land on the same cell. If (r1, c1) == (r2, c2), you must count cherries only once.

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

# Correct: subtract overlap
if r1 == r2 and c1 == c2:
    res -= grid[r1][c1]

Ignoring Blocked Cells (-1) Properly

Returning 0 instead of a large negative value when hitting a thorn. Returning 0 makes invalid paths appear viable when taking the maximum. Return -1000 or float('-inf') to ensure invalid paths are never selected.

Forgetting to Handle No Valid Path

Not checking if the final result is negative before returning. If no valid path exists (blocked by thorns), the algorithm may return a negative number. The answer should be max(0, result).

Using 4D State Without Optimization

Not recognizing that c2 can be derived from c1, r1, and r2 since both people take the same number of steps (r1 + c1 == r2 + c2). This optimization reduces state space from O(n^4) to O(n^3).