|
| 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