Skip to content

Commit 78cea0b

Browse files
committed
feat(algorithms, dynamic programming): cherry pickup
1 parent bd90354 commit 78cea0b

7 files changed

Lines changed: 343 additions & 0 deletions

File tree

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# Cherry Pickup
2+
3+
You are given an n x n grid representing a field of cherries, each cell is one of three possible integers.
4+
5+
- 0 means the cell is empty, so you can pass through,
6+
- 1 means the cell contains a cherry that you can pick up and pass through, or
7+
- -1 means the cell contains a thorn that blocks your way.
8+
9+
Return the maximum number of cherries you can collect by following the rules below:
10+
11+
- Starting at the position (0, 0) and reaching (n - 1, n - 1) by moving right or down through valid path cells (cells
12+
with value 0 or 1).
13+
- After reaching (n - 1, n - 1), returning to (0, 0) by moving left or up through valid path cells.
14+
- When passing through a path cell containing a cherry, you pick it up, and the cell becomes an empty cell 0.
15+
- If there is no valid path between (0, 0) and (n - 1, n - 1), then no cherries can be collected.
16+
17+
> Note: A valid path requires traveling from the top-left corner to the bottom-right corner and successfully returning
18+
> to the starting point.
19+
20+
## Constraints
21+
22+
- n == `grid.length`
23+
- n == `grid[i].length`
24+
- 1 <= `n` <= 50
25+
- `grid[i][j]` is -1, 0, or 1.
26+
- `grid[0][0]` != -1
27+
- `grid[n - 1][n - 1]` != -1
28+
29+
## Examples
30+
31+
![Example 1](./images/examples/cherry_pickup_example_1.png)
32+
![Example 2](./images/examples/cherry_pickup_example_2.png)
33+
![Example 3](./images/examples/cherry_pickup_example_3.png)
34+
35+
Example 4
36+
37+
![Example 4](./images/examples/cherry_pickup_example_4.png)
38+
39+
```text
40+
Input: grid = [[0,1,-1],[1,0,-1],[1,1,1]]
41+
Output: 5
42+
Explanation: The player started at (0, 0) and went down, down, right right to reach (2, 2).
43+
4 cherries were picked up during this single trip, and the matrix becomes [[0,1,-1],[0,0,-1],[0,0,0]].
44+
Then, the player went left, up, up, left to return home, picking up one more cherry.
45+
The total number of cherries picked up is 5, and this is the maximum possible.
46+
```
47+
48+
Example 5
49+
50+
```text
51+
Input: grid = [[1,1,-1],[1,-1,1],[-1,1,1]]
52+
Output: 0
53+
```
54+
55+
## Topics
56+
57+
- Array
58+
- Dynamic Programming
59+
- Matrix
60+
61+
## Solution(s)
62+
63+
1. [Dynamic Programming (Top Down)](#dynamic-programming-top-down)
64+
2. [Dynamic Programming (Top Down) 2](#dynamic-programming-top-down-2)
65+
3. [Dynamic Programming (Bottom Up)](#dynamic-programming-bottom-up)
66+
67+
### Dynamic Programming (Top Down)
68+
69+
Instead of walking from end to beginning, let's reverse the second leg of the path, so we are only considering two paths
70+
from the beginning to the end.
71+
72+
Notice after `t` steps, each position `(r, c)` we could be, is on the line `r + c = t`. So if we have two people at
73+
positions `(r1, c1)` and `(r2, c2)`, then `r2 = r1 + c1 - c2`. That means the variables `r1`, `c1`, `c2` uniquely
74+
determine 2 people who have walked the same `r1 + c1` number of steps. This sets us up for dynamic programming quite
75+
nicely.
76+
77+
Let `dp[r1][c1][c2]` be the most number of cherries obtained by two people starting at `(r1, c1)` and `(r2, c2)` and
78+
walking towards `(n - 1, n - 1)` picking up cherries, where `r2 = r1 + c1 - c2`.
79+
80+
If `grid[r1][c1]` and `grid[r2][c2]` are not thorns, then the value of `dp[r1][c1][c2]` is (`grid[r1][c1]` + `grid[r2][c2]`),
81+
plus the maximum of `dp[r1 + 1][c1][c2]`, `dp[r1][c1 + 1][c2]`, `dp[r1 + 1][c1][c2 + 1]`, `dp[r1][c1 + 1][c2 + 1]` as
82+
appropriate. We should also be careful to not double count in case `(r1, c1) == (r2, c2)`.
83+
84+
Why did we say it was the maximum of `dp[r + 1][c1][c2]` etc.? It corresponds to the 4 possibilities for persons 1 and
85+
2 moving down and right:
86+
87+
- Person 1 down and person 2 down: `dp[r1 + 1][c1][c2]`;
88+
- Person 1 right and person 2 down: `dp[r1][c1 + 1][c2]`;
89+
- Person 1 down and person 2 right: `dp[r1 + 1][c1][c2 + 1]`;
90+
- Person 1 right and person 2 right: `dp[r1][c1 + 1][c2 + 1]`;
91+
92+
#### Complexity
93+
94+
- **Time Complexity**: `O(n^3)`, where `n` is the length of grid. Our dynamic programming has `n^3` states, and each
95+
state is calculated once.
96+
- **Space Complexity**: `O(n^3)`, the size of memo.
97+
98+
99+
100+
### Dynamic Programming (Top Down) 2
101+
102+
The naive approach might be to find the optimal path from (0, 0) to (n-1, n-1), collect cherries along the way, then
103+
find the optimal return path on the modified grid. However, this greedy approach fails because the first path might block
104+
better options for the return journey.
105+
106+
The key insight is recognizing that going from (0, 0) to (n-1, n-1) and back is equivalent to having two people start
107+
simultaneously from (0, 0) and both reach (n-1, n-1). Why? Because any path from start to end and back can be split into
108+
two paths from start to end - one person follows the original forward path, and another follows the return path in
109+
reverse.
110+
111+
Since both people move simultaneously, after k steps, if person 1 is at (i1, j1) and person 2 is at (i2, j2), we know
112+
that i1 + j1 = k and i2 + j2 = k. This means we only need to track the row positions i1 and i2, as the column positions
113+
can be derived: j1 = k - i1 and j2 = k - i2.
114+
115+
This transforms our problem into a 3-dimensional DP: `f[k][i1][i2]` represents the maximum cherries collected when both
116+
people have taken k steps, with person 1 at row i1 and person 2 at row i2.
117+
118+
At each step, both people can move either right or down (equivalent to coming from left or up in the previous step). This
119+
gives us four possible combinations of previous positions to consider. When both people land on the same cell, we count
120+
that cherry only once.
121+
122+
The total number of steps to reach (n-1, n-1) from (0, 0) is 2n-2 (we need to move n-1 steps right and n-1 steps down).
123+
The final answer is the maximum cherries collected when both people reach (n-1, n-1) after 2n-2 steps.
124+
125+
#### Algorithm
126+
127+
The implementation uses dynamic programming with three dimensions. Let's walk through the key components:
128+
129+
1. **State Definition**:
130+
- `f[k][i1][i2]` represents the maximum cherries collected when both persons have taken k steps total
131+
- Person 1 is at position (i1, k-i1)
132+
- Person 2 is at position (i2, k-i2)
133+
134+
2. **Initialization**
135+
- Create a 3D array with dimensions (2n-1) × n × n
136+
- Initialize all values to negative infinity (-inf) to represent invalid states
137+
- Set `f[0][0][0]` = `grid[0][0]` as both persons start at (0, 0)
138+
139+
3. **State Transition**: For each step `k` from 1 to `2n-2`
140+
- Iterate through all possible positions (i1, j1) for person 1 where j1 = k - i1
141+
- Iterate through all possible positions (i2, j2) for person 2 where j2 = k - i2
142+
- Check validity
143+
- Both j1 and j2 must be within bounds [0, n)
144+
- Neither `grid[i1][j1]` nor `grid[i2][j2]` should be -1 (thorn)
145+
- Calculate cherries collected at current positions:
146+
- If both persons are at the same cell (i1 == i2), count cherry once: t = `grid[i1][j1]`
147+
- If at different cells, sum both: t = `grid[i1][j1]` + `grid[i2][j2]`
148+
- Consider all valid previous positions:
149+
- Person 1 could have come from (i1-1, j1) or (i1, j1-1), meaning x1 ∈ {i1-1, i1}
150+
- Person 2 could have come from (i2-1, j2) or (i2, j2-1), meaning x2 ∈ {i2-1, i2}
151+
- Update: `f[k][i1][i2] = max(f[k][i1][i2], f[k-1][x1][x2] + t)`
152+
153+
4. **Final Answer**
154+
- The maximum cherries when both reach (n-1, n-1) is `f[2n-2][n-1][n-1]`
155+
- Return `max(0, f[2n-2][n-1][n-1])` to handle cases where no valid path exists (result would be negative infinity)
156+
157+
#### Complexity
158+
159+
- **Time Complexity: O(n^3)**
160+
The algorithm uses dynamic programming with three nested loops:
161+
- The outermost loop iterates through `k` from `1` to `2n - 2`, which is `O(n)` iterations
162+
- The second loop iterates through `i1` from `0` to `n - 1`, which is `O(n)` iterations
163+
- The third loop iterates through `i2` from `0` to `n - 1`, which is `O(n)` iterations
164+
- Inside these three loops, there are two more nested loops for `x1` and `x2`, but these only iterate through at most
165+
2 values each (from i - 1 to i), making them `O(1)`
166+
167+
Therefore, the overall time complexity is `O(n) × O(n) × O(n) × O(1) = O(n^3)`.
168+
169+
- **Space Complexity: O(n^3)**
170+
The algorithm creates a 3D DP array `f` with dimensions:
171+
- First dimension: `(2n - 1)` elements, which is `O(n)`
172+
- Second dimension: `n` elements, which is `O(n)`
173+
- Third dimension: `n` elements, which is `O(n)`
174+
175+
The total space used by the DP array is `O(n) × O(n) × O(n) = O(n^3)`. Where `n` is the side length of the grid.
176+
177+
178+
### Dynamic Programming (Bottom Up)
179+
180+
Like in [the first approach](#dynamic-programming-top-down), we have the idea of dynamic programming.
181+
182+
Say `r1 + c1 = t` is the t-th layer. Since our recursion only references the next layer, we only need to keep two layers
183+
in memory at a time.
184+
185+
At time `t`, let `dp[c1][c2]` be the most cherries that we can pick up for two people going from `(0, 0)` to `(r1, c1)`
186+
and `(0, 0)` to `(r2, c2)`, where `r1 = t-c1`, `r2 = t-c2`. Our dynamic program proceeds similarly to [the first approach](#dynamic-programming-top-down).
187+
188+
#### Complexity Analysis
189+
190+
- **Time Complexity**: `O(n^3`), where n is the length of grid. We have three for-loops of size n.
191+
- **Space Complexity**: `O(n^2)`, the sizes of `dp` and `dp2`.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from typing import List, Optional
2+
from math import inf
3+
4+
5+
def cherry_pickup_dp_top_down(grid: List[List[int]]) -> int:
6+
n = len(grid)
7+
memo = [[[None] * n for _ in range(n)] for _ in range(n)]
8+
9+
def dp(r1: int, c1: int, c2: int) -> Optional[int | float]:
10+
r2 = r1 + c1 - c2
11+
if (
12+
n == r1
13+
or n == r2
14+
or n == c1
15+
or n == c2
16+
or grid[r1][c1] == -1
17+
or grid[r2][c2] == -1
18+
):
19+
return -inf
20+
elif r1 == c1 == n - 1:
21+
return grid[r1][c1]
22+
elif memo[r1][c1][c2] is not None:
23+
return memo[r1][c1][c2]
24+
else:
25+
result = grid[r1][c1] + (c1 != c2) * grid[r2][c2]
26+
result += max(
27+
dp(r1, c1 + 1, c2 + 1),
28+
dp(r1 + 1, c1, c2 + 1),
29+
dp(r1, c1 + 1, c2),
30+
dp(r1 + 1, c1, c2),
31+
)
32+
memo[r1][c1][c2] = result
33+
return result
34+
35+
return max(0, dp(0, 0, 0))
36+
37+
38+
def cherry_pickup_dp_top_down_2(grid: List[List[int]]) -> int:
39+
n = len(grid)
40+
41+
# dp[k][i1][i2] represents the maximum cherries collected when:
42+
# - Person 1 is at position (i1, j1) where j1 = k - i1
43+
# - Person 2 is at position (i2, j2) where j2 = k - i2
44+
# - k represents the sum of coordinates (i + j), indicating the step number
45+
dp = [[[-inf] * n for _ in range(n)] for _ in range(2 * n - 1)]
46+
47+
# Initialize starting position - both persons start at (0, 0)
48+
dp[0][0][0] = grid[0][0]
49+
50+
# Iterate through each step (k represents Manhattan distance from origin)
51+
for k in range(1, 2 * n - 1):
52+
for row1 in range(n):
53+
for row2 in range(n):
54+
# Calculate column positions based on current step and row
55+
col1 = k - row1
56+
col2 = k - row2
57+
58+
# Check if positions are valid and not blocked by thorns
59+
if (
60+
not 0 <= col1 < n
61+
or not 0 <= col2 < n
62+
or grid[row1][col1] == -1
63+
or grid[row2][col2] == -1
64+
):
65+
continue
66+
67+
# Calculate cherries to pick at current positions
68+
cherries_picked = grid[row1][col1]
69+
# Avoid double counting if both persons are at the same cell
70+
if row1 != row2:
71+
cherries_picked += grid[row2][col2]
72+
73+
# Check all possible previous positions for both persons
74+
# Each person could have come from either up or left
75+
for prev_row1 in range(row1 - 1, row1 + 1):
76+
for prev_row2 in range(row2 - 1, row2 + 1):
77+
# Ensure previous positions are within bounds
78+
if prev_row1 >= 0 and prev_row2 >= 0:
79+
dp[k][row1][row2] = max(
80+
dp[k][row1][row2],
81+
dp[k - 1][prev_row1][prev_row2] + cherries_picked,
82+
)
83+
84+
# Return the maximum cherries collected, or 0 if no valid path exists
85+
return max(0, dp[-1][-1][-1])
86+
87+
88+
def cherry_pickup_dp_bottom_up(grid: List[List[int]]) -> int:
89+
n = len(grid)
90+
dp = [[-inf] * n for _ in range(n)]
91+
dp[0][0] = grid[0][0]
92+
93+
for t in range(1, 2 * n - 1):
94+
dp2 = [[-inf] * n for _ in range(n)]
95+
for i in range(max(0, t - (n - 1)), min(n - 1, t) + 1):
96+
for j in range(max(0, t - (n - 1)), min(n - 1, t) + 1):
97+
if grid[i][t - i] == -1 or grid[j][t - j] == -1:
98+
continue
99+
val = grid[i][t - i]
100+
if i != j:
101+
val += grid[j][t - j]
102+
dp2[i][j] = max(
103+
dp[pi][pj] + val
104+
for pi in (i - 1, i)
105+
for pj in (j - 1, j)
106+
if pi >= 0 and pj >= 0
107+
)
108+
dp = dp2
109+
return max(0, dp[n - 1][n - 1])
50.2 KB
Loading
51.8 KB
Loading
48.7 KB
Loading
74.1 KB
Loading
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import unittest
2+
from typing import List
3+
from parameterized import parameterized
4+
from utils.test_utils import custom_test_name_func
5+
from algorithms.dynamic_programming.cherry_pickup import (
6+
cherry_pickup_dp_top_down,
7+
cherry_pickup_dp_top_down_2,
8+
cherry_pickup_dp_bottom_up,
9+
)
10+
11+
12+
CHERRY_PICKUP_TEST_CASES = [
13+
([[0, 1, -1], [1, 0, -1], [1, 1, 1]], 5),
14+
([[1, 1, -1], [1, -1, 1], [-1, 1, 1]], 0),
15+
([[1, 1], [1, 1]], 4),
16+
([[0, -1], [1, 1]], 2),
17+
([[1]], 1),
18+
([[0, 1, 1, 0], [1, -1, 1, 1], [1, 1, -1, 1], [0, 1, 1, 1]], 11),
19+
([[1, 1, 0, 0], [-1, 0, -1, 0], [1, 0, -1, 1], [-1, 1, 1, 1]], 6),
20+
([[0, 1, 1, 0], [0, -1, 1, -1], [1, 0, -1, 1], [-1, 1, 1, 0]], 3),
21+
([[1, 1, 0, 0], [0, -1, 1, -1], [-1, 0, -1, 1], [-1, 1, 0, 0]], 0),
22+
]
23+
24+
25+
class CherryPickupTestCase(unittest.TestCase):
26+
@parameterized.expand(CHERRY_PICKUP_TEST_CASES, name_func=custom_test_name_func)
27+
def test_cherry_pickup_dp_top_down(self, grid: List[List[int]], expected: int):
28+
actual = cherry_pickup_dp_top_down(grid)
29+
self.assertEqual(expected, actual)
30+
31+
@parameterized.expand(CHERRY_PICKUP_TEST_CASES, name_func=custom_test_name_func)
32+
def test_cherry_pickup_dp_top_down_2(self, grid: List[List[int]], expected: int):
33+
actual = cherry_pickup_dp_top_down_2(grid)
34+
self.assertEqual(expected, actual)
35+
36+
@parameterized.expand(CHERRY_PICKUP_TEST_CASES, name_func=custom_test_name_func)
37+
def test_cherry_pickup_dp_bottom_up(self, grid: List[List[int]], expected: int):
38+
actual = cherry_pickup_dp_bottom_up(grid)
39+
self.assertEqual(expected, actual)
40+
41+
42+
if __name__ == "__main__":
43+
unittest.main()

0 commit comments

Comments
 (0)