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
- 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). - Base case: If either person goes out of bounds or lands on a thorn (
-1), return a large negative value to invalidate this path. - Base case: If both reach the destination
(n-1, n-1), return the cherry value at that cell. - Try all 4 combinations of moves: both down, both right, one down and one right, or vice versa.
- Add cherries from both current positions, but subtract one if they are on the same cell.
- 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:
- Space complexity: 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
- Create a 4D DP array initialized to negative infinity to track visited states.
- Use the same recursive function
dfs(r1, c1, r2, c2)as before. - Before computing, check if the current state is already cached in the DP array. If so, return the cached value.
- After computing the result for a state, store it in the DP array before returning.
- 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:
- Space complexity:
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
- Create a 3D DP array using only
(r1, c1, r2)as state variables. - Compute
c2dynamically asc2 = r1 + c1 - r2. - Check bounds for all four coordinates, including the computed
c2. - Base case: When person 1 reaches
(n-1, n-1), return the cherry value. - Try all 4 move combinations, recursively compute the maximum cherries, and cache the result.
- 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:
- Space complexity:
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
- Create a 3D DP array with dimensions
[n][n][n]. - Iterate from
(n-1, n-1, n-1)down to(0, 0, 0)for(r1, c1, r2). - For each state, compute
c2 = r1 + c1 - r2. Skip ifc2is out of bounds. - Skip states where either position lands on a thorn.
- Base case: If at destination, store the cherry value.
- Otherwise, look at all 4 possible next states (from the already computed future states) and take the maximum.
- Add current cherries, avoiding double counting when both are on the same cell.
- Return
dp[0][0][0], clamped to0if 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:
- Space complexity:
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
- Create a 2D
prevarray of size[n][n]representing states for the previous step count. - Initialize
prev[0][0]with the starting cell's cherry value. - For each step count
kfrom1to2n-2:- Create a new 2D
dparray for the current step. - For each valid
(r1, r2)pair wherec1 = k - r1andc2 = k - r2are within bounds:- Look at all 4 transitions from the previous step.
- Take the maximum and add current cherries (avoiding double counting).
- Set
prev = dpfor the next iteration.
- Create a new 2D
- Return
prev[n-1][n-1], clamped to0if 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:
- Space complexity:
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).