Skip to content

Commit e48b754

Browse files
committed
Complete Chapter 2 (DP) and open Chapter 3 (Arrays & Two Pointers)
Chapter 2 is now complete (13 problems): - Frog Jump (set-valued state, bottom-up + top-down) - Super Egg Drop (state inversion) - Minimum Cost To Cut A Stick (interval DP) - Minimum Cost To Merge Stones (interval DP + pile-count dimension) - Maximum Profit In Job Scheduling (sort + binary-search DP) - Closest Subsequence Sum stub linking to the 1.22 treatment New Chapter 3 (Arrays, Two Pointers & Matrices): - Pattern primer (4 pointer dances + complexity intuition) - Two Sum II, Three Sum, Move Zeroes, Merge Intervals, Insert Interval, Rotate Image, Spiral Matrix — each with intuition, multiple approaches, 5-language code with @param/@return docs, dry runs, math-backed complexity All code verified: Python executed against test cases; Kotlin sources compile with kotlinc 2.1.20; tabs verified via jsdom on chapter 3 pages.
1 parent 80ccb24 commit e48b754

16 files changed

Lines changed: 3088 additions & 0 deletions

‎CodingInterviewFightClub/src/SUMMARY.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,19 @@
4141
- [2.5 Unbounded Knapsack](ch02-dynamic-programming/unbounded-knapsack.md)
4242
- [2.6 Partition Equal Subset Sum](ch02-dynamic-programming/partition-equal-subset-sum.md)
4343
- [2.7 Maximum Product Subarray](ch02-dynamic-programming/maximum-product-subarray.md)
44+
- [2.8 Frog Jump](ch02-dynamic-programming/frog-jump.md)
45+
- [2.9 Super Egg Drop](ch02-dynamic-programming/super-egg-drop.md)
46+
- [2.10 Minimum Cost To Cut A Stick](ch02-dynamic-programming/minimum-cost-to-cut-a-stick.md)
47+
- [2.11 Minimum Cost To Merge Stones](ch02-dynamic-programming/minimum-cost-to-merge-stones.md)
48+
- [2.13 Maximum Profit In Job Scheduling](ch02-dynamic-programming/maximum-profit-in-job-scheduling.md)
49+
50+
- [3. Arrays, Two Pointers & Matrices](ch03-arrays/index.md)
51+
- [3.0 Pattern Primer: Two Pointers & The Sorted-Array Dance](ch03-arrays/pattern-primer.md)
52+
- [3.1 Two Sum II (Sorted)](ch03-arrays/two-sum-ii.md)
53+
- [3.2 Three Sum](ch03-arrays/three-sum.md)
54+
- [3.3 Move Zeroes](ch03-arrays/move-zeroes.md)
55+
- [3.4 Merge Intervals](ch03-arrays/merge-intervals.md)
56+
- [3.5 Insert Interval](ch03-arrays/insert-interval.md)
57+
- [3.6 Rotate Image](ch03-arrays/rotate-image.md)
58+
- [3.7 Spiral Matrix](ch03-arrays/spiral-matrix.md)
59+
- [2.12 Closest Subsequence Sum](ch02-dynamic-programming/closest-subsequence-sum.md)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# 2.12 Closest Subsequence Sum
2+
3+
This problem is covered in full in [Chapter 1, section 1.22](../ch01-binary-search/closest-subsequence-sum.md), because its core weapon is **meet-in-the-middle + binary search** — a Chapter 1 pattern — rather than a DP table.
4+
5+
It earns a mention in the DP chapter precisely because it is the **anti-DP**: enumerating all $2^{n/2}$ subset sums per half beats any $O(n \cdot \text{range})$ table when values are huge. Read the full treatment there, then come back and ask yourself: *"when does DP stop being the right tool?"*
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
# 2.8 Frog Jump
2+
3+
> **Source:** [`src/main/kotlin/dynamic_programming/FrogJump.kt`](https://github.com/arpanpathak/AdvancedAlgorithmPatterns/blob/main/src/main/kotlin/dynamic_programming/FrogJump.kt) · [`FrogJumpTopDown.kt`](https://github.com/arpanpathak/AdvancedAlgorithmPatterns/blob/main/src/main/kotlin/dynamic_programming/FrogJumpTopDown.kt)
4+
> **Pattern:** set-valued DP state · **Gym page**
5+
6+
## The Problem
7+
8+
A frog is crossing a river on stones. `stones[i]` is the position of the i-th stone (strictly increasing, starting at 0). The frog starts on stone 0 and must land on the last stone.
9+
10+
Rule: if the frog's **last jump** was `k` units, its next jump must be exactly `k-1`, `k`, or `k+1` units (and `k > 0`). The first jump is always exactly 1 unit. Can the frog make it?
11+
12+
- Constraints: $2 \le n \le 2000$, positions up to $2^{31}-1$.
13+
14+
## Examples
15+
16+
```
17+
Input: stones = [0, 1, 3, 5, 6, 8, 12, 17]
18+
Output: true
19+
Explanation: 0→1 (k=1), 1→3 (k=2), 3→5 (k=2), 5→8 (k=3), 8→12 (k=4), 12→17 (k=5)
20+
21+
Input: stones = [0, 1, 2, 3, 4, 8, 9, 11]
22+
Output: false
23+
Explanation: from 4 the reachable jumps are k-1,k,k+1 of the jump that arrived;
24+
the gap to 8 can't be made under the rule.
25+
```
26+
27+
## Intuition — the state is *"what jumps can land me here?"*
28+
29+
The tricky part: a position alone doesn't determine the future — the **last jump size `k`** matters (it constrains the next jump). So the state must be a pair:
30+
31+
$$
32+
\text{state} = (\text{stone position } p, \text{last jump } k)
33+
$$
34+
35+
There are two classic encodings, and the repo ships **both**:
36+
37+
**Bottom-up (FrogJump.kt):** `stoneMap[p]` = the **set of jump sizes `k`** with which the frog can *arrive* at position `p`. Propagate:
38+
39+
```
40+
for each stone p, for each k in stoneMap[p]:
41+
for step in {k-1, k, k+1} (step > 0):
42+
if p + step is a stone: add step to stoneMap[p + step]
43+
```
44+
45+
`stoneMap` values are sets → the "state space" is the set of reachable (position, jump) pairs. Each pair is processed once → $O(n^2)$ total (each stone holds at most $O(n)$ jumps).
46+
47+
**Top-down (FrogJumpTopDown.kt):** `solve(pos, k)` = "can I reach the last stone from `pos`, given last jump `k`?" — memoized over the `(pos, k)` pairs. Same state space, recursion-first style.
48+
49+
**Why sets, not a boolean?** Two frogs can reach the same stone with *different* last jumps, and those different `k`s lead to different futures. A single boolean "reachable" discards exactly the information the rule needs. The set is the honest state.
50+
51+
## Approach 1 — Bottom-up with reachable-jump sets (the repo's first version)
52+
53+
```kotlin
54+
/**
55+
* @param stones the positions of the stones, strictly increasing, starting at 0
56+
* @return true iff the frog can reach the last stone under the k-1/k/k+1 rule
57+
*/
58+
fun canCross(stones: IntArray): Boolean {
59+
// The very first jump is fixed: 0 -> 1.
60+
if (stones[1] != 1) return false
61+
62+
// Map: stone position -> set of jump sizes 'k' that can land on this stone.
63+
val stoneMap = mutableMapOf<Int, MutableSet<Int>>()
64+
stones.forEach { stone -> stoneMap[stone] = mutableSetOf() }
65+
stoneMap[0]?.add(0) // the "jump" that arrives at the start
66+
67+
for (stone in stones) {
68+
for (k in stoneMap[stone]!!) {
69+
for (step in k - 1..k + 1) {
70+
if (step > 0) {
71+
val nextStone = stone + step
72+
if (stoneMap.containsKey(nextStone)) { // O(1) stone lookup
73+
stoneMap[nextStone]?.add(step)
74+
}
75+
}
76+
}
77+
}
78+
}
79+
return stoneMap[stones.last()]?.isNotEmpty() ?: false
80+
}
81+
```
82+
83+
## Approach 2 — Top-down memoized recursion (the repo's second version)
84+
85+
```kotlin
86+
/**
87+
* @param stones the positions of the stones, strictly increasing, starting at 0
88+
* @return true iff the frog can reach the last stone under the k-1/k/k+1 rule
89+
*/
90+
fun canCross(stones: IntArray): Boolean {
91+
val stoneSet = stones.toSet() // O(1) membership tests
92+
val cache = mutableMapOf<Pair<Int, Int>, Boolean>()
93+
94+
/**
95+
* @param pos the current stone position
96+
* @param k the size of the last jump used to reach pos
97+
* @return true iff the last stone is reachable from (pos, k)
98+
*/
99+
fun isValidJump(pos: Int, nextJump: Int) = nextJump > 0 && (pos + nextJump) in stoneSet
100+
101+
fun solve(pos: Int, k: Int): Boolean =
102+
cache.getOrPut(Pair(pos, k)) {
103+
pos == stones.last() || (k - 1..k + 1).any { nextJump ->
104+
isValidJump(pos, nextJump) && solve(pos + nextJump, nextJump)
105+
}
106+
}
107+
108+
return solve(0, 0)
109+
}
110+
```
111+
112+
```java
113+
import java.util.*;
114+
115+
public class FrogJump {
116+
/**
117+
* @param stones the positions of the stones, strictly increasing, starting at 0
118+
* @return true iff the frog can reach the last stone under the k-1/k/k+1 rule
119+
*/
120+
public boolean canCross(int[] stones) {
121+
Map<Integer, Set<Integer>> stoneMap = new HashMap<>();
122+
for (int s : stones) stoneMap.put(s, new HashSet<>());
123+
stoneMap.get(0).add(0); // "arrival" jump at the start
124+
125+
for (int stone : stones) {
126+
for (int k : stoneMap.get(stone)) {
127+
for (int step = k - 1; step <= k + 1; step++) {
128+
if (step > 0 && stoneMap.containsKey(stone + step)) {
129+
stoneMap.get(stone + step).add(step);
130+
}
131+
}
132+
}
133+
}
134+
return !stoneMap.get(stones[stones.length - 1]).isEmpty();
135+
}
136+
}
137+
```
138+
139+
```cpp
140+
#include <vector>
141+
#include <unordered_map>
142+
#include <unordered_set>
143+
144+
class FrogJump {
145+
public:
146+
/**
147+
* @param stones the positions of the stones, strictly increasing, starting at 0
148+
* @return true iff the frog can reach the last stone under the k-1/k/k+1 rule
149+
*/
150+
bool canCross(const std::vector<int>& stones) {
151+
std::unordered_map<int, std::unordered_set<int>> stoneMap;
152+
for (int s : stones) stoneMap[s];
153+
stoneMap[0].insert(0);
154+
155+
for (int stone : stones) {
156+
for (int k : stoneMap[stone]) {
157+
for (int step = k - 1; step <= k + 1; step++) {
158+
if (step > 0 && stoneMap.count(stone + step)) {
159+
stoneMap[stone + step].insert(step);
160+
}
161+
}
162+
}
163+
}
164+
return !stoneMap[stones.back()].empty();
165+
}
166+
};
167+
```
168+
169+
```python
170+
def can_cross(stones: list[int]) -> bool:
171+
"""
172+
@param stones: the positions of the stones, strictly increasing, starting at 0
173+
@return: True iff the frog can reach the last stone under the k-1/k/k+1 rule
174+
"""
175+
stone_map: dict[int, set[int]] = {s: set() for s in stones}
176+
stone_map[0].add(0) # "arrival" jump at the start
177+
178+
for stone in stones:
179+
for k in list(stone_map[stone]):
180+
for step in (k - 1, k, k + 1):
181+
if step > 0 and (stone + step) in stone_map:
182+
stone_map[stone + step].add(step)
183+
return bool(stone_map[stones[-1]])
184+
```
185+
186+
```rust
187+
use std::collections::{HashMap, HashSet};
188+
189+
impl Solution {
190+
/// @param stones the positions of the stones, strictly increasing, starting at 0
191+
/// @return true iff the frog can reach the last stone under the k-1/k/k+1 rule
192+
pub fn can_cross(stones: Vec<i32>) -> bool {
193+
let mut stone_map: HashMap<i32, HashSet<i32>> =
194+
stones.iter().map(|&s| (s, HashSet::new())).collect();
195+
stone_map.get_mut(&0).unwrap().insert(0); // "arrival" jump at the start
196+
197+
for &stone in &stones {
198+
let jumps: Vec<i32> = stone_map[&stone].iter().cloned().collect();
199+
for k in jumps {
200+
for step in (k - 1)..=(k + 1) {
201+
if step > 0 && stone_map.contains_key(&(stone + step)) {
202+
stone_map.get_mut(&(stone + step)).unwrap().insert(step);
203+
}
204+
}
205+
}
206+
}
207+
!stone_map[stones.last().unwrap()].is_empty()
208+
}
209+
}
210+
```
211+
212+
## Dry run
213+
214+
**Input:** `stones = [0, 1, 3, 5, 6, 8, 12, 17]`. Bottom-up propagation:
215+
216+
```
217+
init: stoneMap[0] = {0}
218+
stone 0: k=0 -> steps 1 -> land on 1 => stoneMap[1] = {1}
219+
stone 1: k=1 -> steps 1,2 -> land on 2?(no), 3 => stoneMap[3] = {2}
220+
stone 3: k=2 -> steps 1,2,3 -> land on 4?(no),5,6 => stoneMap[5]={2}, stoneMap[6]={3}
221+
stone 5: k=2 -> steps 1,2,3 -> land on 6,7?(no),8 => stoneMap[6]={3,2}, stoneMap[8]={3}
222+
stone 6: k=3 -> steps 2,3,4 -> land on 8,9?(no),10? => stoneMap[8]={3,2}
223+
k=2 -> steps 1,2,3 -> land on 7,8,9 -> stoneMap[8]={3,2} (2 already there)
224+
stone 8: k=3 -> steps 2,3,4 -> land on 10?,11?,12 => stoneMap[12]={4}
225+
k=2 -> steps 1,2,3 -> land on 9,10,11 -> nothing new
226+
stone 12: k=4 -> steps 3,4,5 -> land on 15?,16?,17 => stoneMap[17]={5} ✓
227+
stone 17: stoneMap[17] = {5} non-empty -> true ✓
228+
```
229+
230+
The answer emerges from watching a single jump size thread through the stones: 0→1 (k=1), 1→3 (k=2), 3→5 (k=2), 5→8 (k=3), 8→12 (k=4), 12→17 (k=5). Note stone 6 gets *two* jumps ({2, 3}) — the set is essential, because the k=3 arrival is what later enables the 8→12 jump.
231+
232+
**Why the O(1) stone lookup matters:** positions are sparse (gaps of any size), so `stoneMap.containsKey` avoids scanning. Without it, "is there a stone at p+step" would be $O(n)$ per probe.
233+
234+
## Complexity
235+
236+
**Time.** Each stone holds up to $O(n)$ jumps; each jump spawns 3 probes:
237+
238+
$$
239+
T(n) = O(n^2) \quad \text{(each (stone, jump) pair processed once)}
240+
$$
241+
242+
**Space.** The map holds one set per stone: $O(n^2)$ worst case.
243+
244+
## Variants & follow-ups
245+
246+
- **House Robber / classic jump games** — different constraints, no jump-size memory; those are simpler 1D DPs. The *memory of the last jump* is what makes this state 2D.
247+
- **Interview follow-up:** "Why can't we use a boolean reachable[] array?" Because `reachable[p]` doesn't record *how* you arrived; the next jump depends on the arrival jump. Two arrivals with different `k` have different futures — the set IS the state.
248+
- **Interview follow-up:** "Can the top-down version skip the memo and still work?" Only for tiny inputs; without memoization `solve(pos, k)` is exponential (each call branches 3 ways and the same `(pos, k)` recurs across many paths). The memo is what makes it $O(n^2)$.

0 commit comments

Comments
 (0)