LeetCode 741 - Cherry Pickup
This problem asks us to collect the maximum possible number of cherries while making two trips across a square grid. The first trip starts at the top left corner (0, 0) and moves to the bottom right corner (n - 1, n - 1) using only right or down moves.
Difficulty: 🔴 Hard
Topics: Array, Dynamic Programming, Matrix
Solution
Cherry Pickup - LeetCode 741
Problem Understanding
This problem asks us to collect the maximum possible number of cherries while making two trips across a square grid.
The first trip starts at the top left corner (0, 0) and moves to the bottom right corner (n - 1, n - 1) using only right or down moves. After reaching the destination, we must return to the starting point using only left or up moves.
The grid contains three kinds of cells:
0means the cell is empty and can be traversed.1means the cell contains a cherry that can be collected.-1means the cell contains a thorn and cannot be traversed.
When a cherry is picked up once, it disappears from the grid. This means the same cherry cannot be collected twice.
The goal is to compute the maximum total number of cherries that can be collected across both trips combined. If no valid path exists from the start to the destination, the answer must be 0.
At first glance, this looks like a pathfinding problem with two separate traversals. However, independently optimizing the forward and backward trips does not work because the choice made during the first trip changes the state of the grid for the second trip.
The constraints are important:
ncan be as large as50- The grid has up to
2500cells
A naive exhaustive search would be far too expensive because the number of possible paths grows exponentially.
Several edge cases are important:
- The destination may be unreachable because of thorns.
- The start or destination cell may contain a cherry.
- Both trips may overlap heavily.
- Multiple optimal paths may share cells.
- We must avoid double counting cherries when two traversals visit the same cell.
The most important conceptual insight is that the forward and backward trips can be transformed into two people walking from (0, 0) to (n - 1, n - 1) simultaneously.
Approaches
Brute Force Approach
The brute force solution tries every possible path from the start to the destination, then for each such path, modifies the grid and tries every possible return path.
A path from (0, 0) to (n - 1, n - 1) contains exactly 2n - 2 moves, and each move has two choices, either right or down. This creates an exponential number of possible paths.
For every forward path, we would need to:
- Simulate collecting cherries.
- Modify the grid.
- Explore every valid return path.
- Track the maximum total cherries.
This approach is correct because it checks all possible combinations of forward and backward journeys. However, it becomes computationally infeasible very quickly.
For n = 50, the number of paths is astronomically large.
Key Insight for the Optimal Solution
The major insight is that the forward trip and backward trip can be modeled as two people walking from the start to the destination at the same time.
Why does this work?
- The forward journey uses only right and down moves.
- The return journey uses only left and up moves.
- If we reverse the return trip, it also becomes a path from the start to the destination using right and down moves.
Now we can think of the problem as:
Two travelers start at
(0,0)and both move toward(n-1,n-1)simultaneously.
At every step:
- Each traveler moves either right or down.
- Both travelers take the same number of total steps.
- If both land on the same cell, we count the cherry only once.
This transformation removes the complicated "modify the grid between trips" issue.
The state becomes:
- Position of traveler 1:
(r1, c1) - Position of traveler 2:
(r2, c2)
Since both travelers have taken the same number of steps:
r1 + c1 = r2 + c2
This allows us to derive one coordinate from the others and reduce the DP state dimension.
We then use dynamic programming with memoization to compute the maximum cherries collectible from each state.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | Exponential | Exponential | Enumerates all forward and backward paths |
| Optimal Dynamic Programming | O(n³) | O(n³) | Uses simultaneous traversal with memoization |
Algorithm Walkthrough
Step 1: Reframe the Problem
Instead of thinking about one person going forward and backward, think of two people starting at (0,0) and moving toward (n-1,n-1) simultaneously.
Each move increases the total number of steps by one.
Step 2: Define the DP State
Let:
dp(r1, c1, r2)
represent the maximum cherries collectible when:
- Traveler 1 is at
(r1, c1) - Traveler 2 is at
(r2, c2)
Since both travelers took the same number of steps:
r1 + c1 = r2 + c2
we can compute:
c2 = r1 + c1 - r2
This reduces the state space from 4 dimensions to 3 dimensions.
Step 3: Handle Invalid States
A state is invalid if:
- Any coordinate goes outside the grid
- Either traveler lands on a thorn cell
For invalid states, return negative infinity so they are never chosen in the maximum calculation.
Step 4: Handle the Destination
If both travelers reach (n-1, n-1), return the value of that cell.
This is the base case.
Step 5: Collect Cherries
At each state:
- Add the cherry at traveler 1's position
- Add the cherry at traveler 2's position
- If both travelers are on the same cell, count it only once
Step 6: Explore the Four Possible Move Combinations
Each traveler can move either right or down.
This creates four combinations:
- Down, Down
- Down, Right
- Right, Down
- Right, Right
Take the maximum result among these recursive calls.
Step 7: Memoize Results
Many states repeat during recursion.
Use memoization to cache results for each state so each state is computed only once.
Step 8: Return the Final Answer
The recursion may return a negative value if no valid path exists.
In that case, return 0.
Otherwise, return the computed maximum.
Why it works
The algorithm works because every valid forward and backward trip pair corresponds exactly to two simultaneous forward traversals. The DP state fully captures the positions of both travelers after the same number of steps. By exploring all move combinations and memoizing results, the algorithm guarantees that every valid pair of paths is considered exactly once. Counting shared cells only once correctly models the cherry removal behavior.
Python Solution
from typing import List
from functools import lru_cache
class Solution:
def cherryPickup(self, grid: List[List[int]]) -> int:
n = len(grid)
@lru_cache(None)
def dp(r1: int, c1: int, r2: int) -> int:
c2 = r1 + c1 - r2
# Out of bounds
if (
r1 >= n or c1 >= n or
r2 >= n or c2 >= n
):
return float('-inf')
# Thorn cell
if (
grid[r1][c1] == -1 or
grid[r2][c2] == -1
):
return float('-inf')
# Reached destination
if r1 == n - 1 and c1 == n - 1:
return grid[r1][c1]
cherries = grid[r1][c1]
# Avoid double counting
if (r1, c1) != (r2, c2):
cherries += grid[r2][c2]
best_next = max(
dp(r1 + 1, c1, r2 + 1), # down, down
dp(r1 + 1, c1, r2), # down, right
dp(r1, c1 + 1, r2 + 1), # right, down
dp(r1, c1 + 1, r2) # right, right
)
cherries += best_next
return cherries
result = dp(0, 0, 0)
return max(0, result)
The implementation starts by defining a memoized recursive function dp.
The state uses three coordinates because the fourth coordinate can be derived from the total number of steps taken. This significantly reduces the state space.
The first section of the function checks whether the state is valid. If either traveler goes out of bounds or lands on a thorn, the state is impossible and returns negative infinity.
The base case occurs when traveler 1 reaches the destination. Since both travelers always take the same number of steps, traveler 2 must also be there.
The code then collects cherries from both positions. If both travelers occupy the same cell, the cherry is counted only once.
Next, the algorithm recursively explores all four move combinations and selects the maximum future value.
Finally, the function returns the current cherries plus the best future result.
The outer function starts the recursion at (0,0) for both travelers and ensures that negative results become 0.
Go Solution
package main
import "math"
func cherryPickup(grid [][]int) int {
n := len(grid)
type State struct {
r1 int
c1 int
r2 int
}
memo := make(map[State]int)
var dp func(int, int, int) int
dp = func(r1, c1, r2 int) int {
c2 := r1 + c1 - r2
// Out of bounds
if r1 >= n || c1 >= n || r2 >= n || c2 >= n {
return math.MinInt32
}
// Thorn cells
if grid[r1][c1] == -1 || grid[r2][c2] == -1 {
return math.MinInt32
}
// Destination
if r1 == n-1 && c1 == n-1 {
return grid[r1][c1]
}
state := State{r1, c1, r2}
if val, exists := memo[state]; exists {
return val
}
cherries := grid[r1][c1]
// Avoid double counting
if r1 != r2 || c1 != c2 {
cherries += grid[r2][c2]
}
bestNext := max(
dp(r1+1, c1, r2+1),
dp(r1+1, c1, r2),
dp(r1, c1+1, r2+1),
dp(r1, c1+1, r2),
)
cherries += bestNext
memo[state] = cherries
return cherries
}
result := dp(0, 0, 0)
if result < 0 {
return 0
}
return result
}
func max(values ...int) int {
best := values[0]
for _, v := range values {
if v > best {
best = v
}
}
return best
}
The Go implementation follows the same algorithmic structure as the Python solution. Since Go does not provide built in memoization decorators like Python's lru_cache, we explicitly maintain a hash map keyed by a custom State struct.
Negative infinity is represented using math.MinInt32.
Go slices are reference types, so the grid is efficiently passed without copying.
Worked Examples
Example 1
grid = [
[0,1,-1],
[1,0,-1],
[1,1,1]
]
Initial State
Both travelers start at (0,0).
| Traveler 1 | Traveler 2 | Cherries |
|---|---|---|
| (0,0) | (0,0) | 0 |
Step 1
Possible moves:
- Down / Down
- Down / Right
- Right / Down
- Right / Right
The optimal choice eventually becomes:
- Traveler 1: Down
- Traveler 2: Right
| Traveler 1 | Traveler 2 | Collected |
|---|---|---|
| (1,0) | (0,1) | 2 |
Step 2
Next optimal move:
| Traveler 1 | Traveler 2 | Collected |
|---|---|---|
| (2,0) | (1,1) | 3 |
Step 3
Next optimal move:
| Traveler 1 | Traveler 2 | Collected |
|---|---|---|
| (2,1) | (2,1) | 4 |
The shared cell counts only once.
Step 4
Final destination:
| Traveler 1 | Traveler 2 | Collected |
|---|---|---|
| (2,2) | (2,2) | 5 |
Final answer:
5
Example 2
grid = [
[1,1,-1],
[1,-1,1],
[-1,1,1]
]
The travelers eventually encounter unavoidable thorn cells.
No valid path exists from the start to the destination.
The DP recursion returns negative infinity, and the final answer becomes:
0
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n³) | There are O(n³) unique DP states, each processed once |
| Space | O(n³) | Memoization stores O(n³) states |
The state space consists of three independent variables:
r1c1r2
Each ranges from 0 to n-1.
The fourth coordinate is derived mathematically, which reduces complexity from O(n⁴) to O(n³).
Each state computes four transitions in constant time, so the total runtime remains O(n³).
Test Cases
from typing import List
from functools import lru_cache
class Solution:
def cherryPickup(self, grid: List[List[int]]) -> int:
n = len(grid)
@lru_cache(None)
def dp(r1: int, c1: int, r2: int) -> int:
c2 = r1 + c1 - r2
if (
r1 >= n or c1 >= n or
r2 >= n or c2 >= n
):
return float('-inf')
if (
grid[r1][c1] == -1 or
grid[r2][c2] == -1
):
return float('-inf')
if r1 == n - 1 and c1 == n - 1:
return grid[r1][c1]
cherries = grid[r1][c1]
if (r1, c1) != (r2, c2):
cherries += grid[r2][c2]
cherries += max(
dp(r1 + 1, c1, r2 + 1),
dp(r1 + 1, c1, r2),
dp(r1, c1 + 1, r2 + 1),
dp(r1, c1 + 1, r2)
)
return cherries
return max(0, dp(0, 0, 0))
solver = Solution()
assert solver.cherryPickup([
[0,1,-1],
[1,0,-1],
[1,1,1]
]) == 5 # provided example
assert solver.cherryPickup([
[1,1,-1],
[1,-1,1],
[-1,1,1]
]) == 0 # no valid path
assert solver.cherryPickup([
[1]
]) == 1 # single cell with cherry
assert solver.cherryPickup([
[0]
]) == 0 # single empty cell
assert solver.cherryPickup([
[1,1],
[1,1]
]) == 4 # all cherries collected
assert solver.cherryPickup([
[1,-1],
[-1,1]
]) == 0 # completely blocked
assert solver.cherryPickup([
[0,1,1],
[1,1,1],
[1,1,0]
]) == 7 # many overlapping optimal paths
assert solver.cherryPickup([
[1,1,1],
[1,-1,1],
[1,1,1]
]) == 8 # must avoid thorn while maximizing cherries
Test Summary
| Test | Why |
|---|---|
[[0,1,-1],[1,0,-1],[1,1,1]] |
Validates standard example |
[[1,1,-1],[1,-1,1],[-1,1,1]] |
Validates unreachable destination |
[[1]] |
Smallest possible grid with cherry |
[[0]] |
Smallest possible empty grid |
[[1,1],[1,1]] |
Ensures all cherries are counted correctly |
[[1,-1],[-1,1]] |
Tests fully blocked traversal |
| Dense cherry grid | Tests overlap handling |
| Grid with central thorn | Tests path rerouting |
Edge Cases
Completely Blocked Paths
A grid may contain thorn placements that make reaching the destination impossible. A naive implementation might still attempt to compute partial paths or accidentally return negative values.
This implementation handles blocked paths by returning negative infinity for invalid states. If all possible transitions are invalid, the final answer becomes negative, and the outer function converts it to 0.
Both Travelers on the Same Cell
During simultaneous traversal, both travelers may land on the same cell at the same time. Without careful handling, the algorithm would count the cherry twice.
The implementation explicitly checks:
if (r1, c1) != (r2, c2):
This guarantees that shared cells contribute only once.
Single Cell Grid
When n = 1, the start and destination are the same cell. A naive recursive solution may mishandle this because no movement occurs.
The base case immediately returns the cell value when the destination is reached, which correctly handles single cell inputs.
Heavy Path Overlap
Sometimes the optimal strategy requires both traversals to reuse many of the same cells. Greedy approaches that try to maximize the first trip independently often fail here.
The simultaneous traversal DP naturally models overlap correctly because both travelers are optimized together rather than independently.