Skip to content

Commit 7f1b4e7

Browse files
committed
Chapter 19 Variant Gallery: capture the repo's cool duplicate implementations — TSP top-down functional DP, LRU linked-list family, Coin Change BFS, N-Queens set-based, short-code gallery, Eulerian trio (circuit/arrangement/de Bruijn), functional-style gallery, Word Ladder II forward BFS, LFU trio, TreeSet sliding median
1 parent 361a53c commit 7f1b4e7

13 files changed

Lines changed: 1384 additions & 0 deletions

‎CodingInterviewFightClub/src/SUMMARY.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,3 +262,16 @@
262262

263263
- [Repo Coverage Index](appendix-repo-coverage-index.md) — every file in src/main/kotlin mapped
264264
- [The Roadmap](appendix-roadmap.md) — the remaining uncovered files
265+
266+
- [Chapter 19 — Variant Gallery](ch19-gallery/index.md)
267+
- [19.0 Pattern Primer: Same Algorithm, Different Costumes](ch19-gallery/pattern-primer.md)
268+
- [19.1 Travelling Salesman — Top-Down Functional](ch19-gallery/travelling-salesman-top-down.md)
269+
- [19.2 LRU Cache — Linked-List Family](ch19-gallery/lru-cache-linked-list-family.md)
270+
- [19.3 Coin Change — Four Implementations](ch19-gallery/coin-change-family.md)
271+
- [19.4 N-Queens — Set-Based Family](ch19-gallery/n-queen-family.md)
272+
- [19.5 The Shorter/Better Gallery](ch19-gallery/short-code-gallery.md)
273+
- [19.6 The Eulerian Family](ch19-gallery/eulerian-family.md)
274+
- [19.7 The Functional-Programming Gallery](ch19-gallery/functional-programming-gallery.md)
275+
- [19.8 Word Ladder II — Three Implementations](ch19-gallery/word-ladder-ii-family.md)
276+
- [19.9 LFU Cache — Three Implementations](ch19-gallery/lfu-cache-family.md)
277+
- [19.10 Sliding Window Median — TreeSet](ch19-gallery/sliding-window-median-treeset.md)
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# 19.3 Coin Change — The Four Implementations, Captured
2+
3+
> **Sources:** [`src/main/kotlin/array/dp/`](https://github.com/arpanpathak/AdvancedAlgorithmPatterns/tree/main/src/main/kotlin/array/dp) — `CoinChange.kt`, `CoinChangeBottomUp.kt`, `CoinChangeBFS.kt`, `CoinChange_II.kt`, `CoinChange_II_BottomUp.kt`
4+
> **Pattern:** variant gallery — one problem, four engines ([2.16](../ch02-dynamic-programming/coin-change.md) covers the bottom-up winner)
5+
6+
## The family map
7+
8+
| File | Engine | What it proves |
9+
|---|---|---|
10+
| `CoinChange.kt` | memoized top-down + `coinChangeCleanAf` bottom-up | both spellings, one file |
11+
| `CoinChangeBottomUp.kt` | bottom-up + a functional `forEach` flavor | the table fill in filter-min style |
12+
| `CoinChangeBFS.kt` | **BFS over amounts** | "fewest coins" is a shortest-path problem |
13+
| `CoinChange_II.kt` / `_BottomUp.kt` | ways-counting DP | the *counting* twin ([2.16](../ch02-dynamic-programming/coin-change.md) variants) |
14+
15+
## The BFS surprise: `CoinChangeBFS.kt`
16+
17+
The coolest of the four: "fewest coins to reach `amount`" is exactly "shortest path from `0` to `amount` in a graph where each coin is an edge `x → x + coin`". BFS finds it in `O(amount · coins)`:
18+
19+
```kotlin
20+
fun coinChangeBFS(coins: IntArray, amount: Int): Int {
21+
if (amount == 0) return 0
22+
23+
val queue = ArrayDeque<Int>().apply { add(0) }
24+
val visited = mutableSetOf(0)
25+
var steps = 0
26+
27+
while (queue.isNotEmpty()) {
28+
steps++
29+
repeat(queue.size) { // one level = one coin
30+
val current = queue.removeFirst()
31+
for (coin in coins) {
32+
val next = current + coin
33+
if (next == amount) return steps
34+
if (next < amount && visited.add(next)) {
35+
queue.add(next)
36+
}
37+
}
38+
}
39+
}
40+
return -1
41+
}
42+
```
43+
44+
**Why it's correct:** every path from 0 to `amount` uses `k` edges = `k` coins, and BFS finds the minimum hop count. **Why `visited`?** Amounts are re-reachable many ways (`0+1+1` vs `0+2`); the first visit is the fewest coins, so revisits are pruned — same as [6.14](../ch06-graphs/rotting-oranges.md)'s `fresh == INF` guard. The `steps` counter with `repeat(queue.size)` is the [5.2](../ch05-trees/binary-tree-level-order-traversal.md) level fence.
45+
46+
**When to reach for it in an interview:** when the problem is *phrased* as reachability ("can you make the amount? what's the minimum number of coins?") — the graph framing is a different intuition that some interviewers love, and it pairs beautifully with the DP as "two views of the same recurrence".
47+
48+
## The counting twin: `CoinChange_II.kt`
49+
50+
"Number of ways" flips the recurrence from `min` to `sum`, and the **loop order matters** — coins outer, amounts inner makes each combination counted once:
51+
52+
```kotlin
53+
// CoinChange_II.kt (bottom-up counting)
54+
fun change(amount: Int, coins: IntArray): Int {
55+
val dp = IntArray(amount + 1)
56+
dp[0] = 1 // one way to make 0: take nothing
57+
58+
for (coin in coins) { // coin loop OUTSIDE
59+
for (a in coin..amount) {
60+
dp[a] += dp[a - coin] // order matters: combinations, not permutations
61+
}
62+
}
63+
return dp[amount]
64+
}
65+
```
66+
67+
The coin-outer loop is the classic "count combinations vs permutations" distinction ([2.6](../ch02-dynamic-programming/partition-equal-subset-sum.md)'s ascending-vs-descending discussion is this exact subtlety). `dp[0] = 1` seeds the empty combination.
68+
69+
## The top-down in the same file: `CoinChange.kt`
70+
71+
```kotlin
72+
class CoinChange {
73+
fun coinChange(coins: IntArray, amount: Int): Int {
74+
val dp = IntArray(amount + 1) { -1 }
75+
return coinChange(coins, amount, dp).let { if (it != Int.MAX_VALUE) it else -1 }
76+
}
77+
78+
private fun coinChange(coins: IntArray, amount: Int, dp: IntArray): Int {
79+
return when {
80+
amount == 0 -> 0
81+
dp[amount] != -1 -> dp[amount]
82+
else -> {
83+
var minCoins = Int.MAX_VALUE
84+
for (coin in coins) {
85+
if (coin <= amount) {
86+
val result = coinChange(coins, amount - coin, dp)
87+
if (result != Int.MAX_VALUE) {
88+
minCoins = minOf(minCoins, 1 + result)
89+
}
90+
}
91+
}
92+
minCoins
93+
}
94+
}.also { dp[amount] = it }
95+
}
96+
}
97+
```
98+
99+
The `Int.MAX_VALUE` sentinel marks "unreachable"; the `.also { dp[amount] = it }` memoizes on every return path (including the base cases — harmless). The `-1` sentinel in the public wrapper distinguishes "impossible" from "0 coins".
100+
101+
## Dry run (BFS)
102+
103+
**Input:** `coins = [1,2,5]`, `amount = 11`.
104+
105+
```
106+
queue=[0], visited={0}, steps=0
107+
steps=1: 0 -> 1, 2, 5. queue=[1,2,5]
108+
steps=2: 1 -> 2(seen),3,6. 2 -> 3(seen),4,7. 5 -> 6(seen),7(seen),10. queue=[3,6,4,7,10]
109+
steps=3: 3 -> 4(seen),5(seen),8. 6 -> 7(seen),8(seen),11 == amount -> return 3 ✓
110+
```
111+
112+
BFS finds 11 at depth 3 (5+5+1) — the `visited` set keeps the frontier small (amounts reached cheaply are never re-expanded). The DP and BFS agree: fewest coins = 3.
113+
114+
## Complexity
115+
116+
**Time.** BFS: `O(amount · coins)` (each amount expanded once); top-down: `O(amount · coins)`; counting: same.
117+
118+
**Space.** BFS: `O(amount)` visited + queue; DP: `O(amount)`.
119+
120+
## Variants & follow-ups
121+
122+
- **Coin Change** ([2.16](../ch02-dynamic-programming/coin-change.md)) — the full page with the bottom-up winner.
123+
- **Minimum Number Of Refueling Stops** ([11.7](../ch11-greedy/minimum-number-of-refueling-stops.md)) — reachability-as-graph again, with a greedy heap instead of BFS.
124+
- **Interview follow-up:** "Which implementation do you prefer?" DP bottom-up — O(amount·coins), no queue, deterministic. The BFS is the *explanation* tool (shortest-path framing); the top-down is the *derivation* tool (brute-force recursion, memoized). Knowing all three is the "I understand this problem" signal.
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# 19.6 The Eulerian Family — Captured
2+
3+
> **Sources:** [`src/main/kotlin/graph/euler/`](https://github.com/arpanpathak/AdvancedAlgorithmPatterns/tree/main/src/main/kotlin/graph/euler) — `circuit/FindEulerianCircuit.kt`, `circuit/path/ValidArrangementOfPairsRecursive.kt`, `circuit/CrackingTheSafe.kt`, `circuit/path/ReconstructItenary.kt` ([17.9](../ch17-advanced-graphs/reconstruct-itinerary.md) covers the itinerary star)
4+
> **Pattern:** variant gallery — Hierholzer's algorithm in three costumes
5+
6+
## The family map
7+
8+
| File | Problem | Twist on Hierholzer |
9+
|---|---|---|
10+
| `FindEulerianCircuit.kt` | find a circuit (start == end) | post-order + `reversed()`; explicit all-edges-used check |
11+
| `ValidArrangementOfPairsRecursive.kt` | Eulerian *path* in a directed graph | degree-difference to pick the start node |
12+
| `CrackingTheSafe.kt` | shortest superstring of all kⁿ passwords | de Bruijn graph: Hierholzer on `n-1`-length prefixes |
13+
| `ReconstructItenary.kt` | lexicographic itinerary | min-heap destinations ([17.9](../ch17-advanced-graphs/reconstruct-itinerary.md)) |
14+
15+
## 1. `FindEulerianCircuit.kt` — the circuit version
16+
17+
The full Hierholzer with an explicit **"all edges used" verification** — the DFS alone can't prove Eulerian-ness; the leftover-edge check does:
18+
19+
```kotlin
20+
class EulerianCircuit(private val graph: Map<Int, List<Int>>) {
21+
22+
fun findEulerianCircuit(): List<Int>? {
23+
if (!hasEulerianCircuit()) return null
24+
25+
val circuit = mutableListOf<Int>()
26+
val remainingEdges = graph.mapValues { it.value.toMutableList() }.toMutableMap()
27+
28+
val startVertex = graph.keys.firstOrNull { graph[it]?.isNotEmpty() == true } ?: return emptyList()
29+
30+
dfsHierholzer(startVertex, remainingEdges, circuit)
31+
32+
// Final check: if any edges remain, the graph was not Eulerian
33+
val allEdgesUsed = remainingEdges.values.all { it.isEmpty() }
34+
return if (allEdgesUsed) circuit.reversed() else null
35+
}
36+
37+
private fun dfsHierholzer(
38+
u: Int,
39+
remainingEdges: MutableMap<Int, MutableList<Int>>,
40+
circuit: MutableList<Int>
41+
) {
42+
// consume edges while available; post-order append = the circuit
43+
while (remainingEdges[u]?.isNotEmpty() == true) {
44+
val v = remainingEdges[u]!!.removeFirst()
45+
dfsHierholzer(v, remainingEdges, circuit)
46+
}
47+
circuit.add(u)
48+
}
49+
50+
private fun hasEulerianCircuit(): Boolean {
51+
// every vertex with edges must have even degree (undirected) — the circuit condition
52+
return graph.all { (_, neighbors) -> neighbors.size % 2 == 0 }
53+
}
54+
}
55+
```
56+
57+
**What's cool:** `hasEulerianCircuit()` is the *precondition* (all degrees even), `allEdgesUsed` is the *postcondition* (the DFS consumed everything), and the `circuit.reversed()` is the post-order inversion — the three-part correctness story [17.9](../ch17-advanced-graphs/reconstruct-itinerary.md) tells with prose, told here with checks.
58+
59+
## 2. `ValidArrangementOfPairsRecursive.kt` — finding the right start
60+
61+
The Eulerian *path* (not circuit) needs a start node with `outDegree - inDegree == 1`; the degree map picks it, defaulting to any node:
62+
63+
```kotlin
64+
class ValidArrangementOfPairsRecursive {
65+
fun validArrangement(pairs: Array<IntArray>): Array<IntArray> {
66+
val graph = mutableMapOf<Int, ArrayDeque<Int>>()
67+
val degree = mutableMapOf<Int, Int>().withDefault { 0 }
68+
69+
// Build graph and track degree difference (out - in)
70+
pairs.forEach { (u, v) ->
71+
graph.getOrPut(u) { ArrayDeque() }.add(v)
72+
degree[u] = degree.getValue(u) + 1
73+
degree[v] = degree.getValue(v) - 1
74+
}
75+
76+
// Find the start node (outDegree > inDegree)
77+
val start = degree.keys.firstOrNull { degree.getValue(it) == 1 } ?: pairs[0][0]
78+
79+
val path = mutableListOf<IntArray>()
80+
81+
// Recursive DFS for Hierholzer's algorithm
82+
fun dfs(u: Int) {
83+
while (graph[u]?.isNotEmpty() == true) {
84+
val v = graph[u]!!.removeFirst()
85+
dfs(v)
86+
path.add(intArrayOf(u, v)) // post-order: edge added after its tail
87+
}
88+
}
89+
90+
dfs(start)
91+
return path.toTypedArray()
92+
}
93+
}
94+
```
95+
96+
**What's cool:** the degree map is *one* pass (`+1` for out, `-1` for in); the start-node rule ("outdegree − indegree = 1") is a single `firstOrNull`; and the edges themselves — not just vertices — are the post-order output, so the path is returned as `[u,v]` pairs without reconstruction. The `withDefault { 0 }` makes every node's degree computable without `getOrDefault` noise.
97+
98+
## 3. `CrackingTheSafe.kt` — the de Bruijn spin
99+
100+
The shortest string containing every `n`-digit password over `k` digits is an Eulerian path in the de Bruijn graph whose nodes are `(n-1)`-length prefixes:
101+
102+
```kotlin
103+
class CrackingTheSafe {
104+
fun crackSafe(n: Int, k: Int): String {
105+
val visited = mutableSetOf<String>()
106+
val result = StringBuilder()
107+
108+
fun dfs(currentPrefix: String) {
109+
for (i in 0 until k) {
110+
val digit = i.toString()
111+
val nextPassword = currentPrefix + digit
112+
113+
if (nextPassword !in visited) {
114+
visited.add(nextPassword)
115+
116+
val nextPrefix = nextPassword.substring(1) // slide the window
117+
dfs(nextPrefix)
118+
119+
result.append(digit) // post-order append
120+
}
121+
}
122+
}
123+
124+
dfs("0".repeat(n - 1))
125+
return result + "0".repeat(n - 1) // close the cycle: the first n-1 digits repeat
126+
}
127+
}
128+
```
129+
130+
**What's cool:** the `visited` set *is* the edge-set (each password is an edge from its `n-1` prefix); the post-order `result.append(digit)` is Hierholzer in miniature; and the trailing `+ "0".repeat(n-1)` closes the cyclic superstring — the classic de Bruijn construction that [17.9](../ch17-advanced-graphs/reconstruct-itinerary.md)'s "Eulerian path" chapter promises and this file delivers.
131+
132+
## Dry run (cracking the safe)
133+
134+
**Input:** `n = 2`, `k = 2` (all 2-digit binary passwords: 00, 01, 10, 11).
135+
136+
```
137+
dfs("0"): digits 0,1:
138+
password "00" (new) -> dfs("0") [already in progress; "00" only once]
139+
"0"+1 = "01" (new) -> dfs("1"): "10" new -> dfs("0"): "00" seen, "01" seen. append "0".
140+
append "0" -> result "00". (from "10" branch: appends 0... )
141+
...
142+
The post-order appends build "00110..." and the final +"0" closes it.
143+
144+
Output: "00110" (or "01100") — length k^n + n - 1 = 5, containing 00, 01, 10, 11 ✓
145+
```
146+
147+
Every `(n-1)`-prefix is a node; every password is an edge; the DFS never repeats an edge, and the post-order append + cycle-closing suffix produces the minimal superstring. This is the "why Eulerian paths matter beyond flight itineraries" answer.
148+
149+
## Complexity
150+
151+
All three: linear in edges with the greedy post-order consumption:
152+
153+
$$
154+
T(V, E) = O(V + E)
155+
$$
156+
157+
**Space.** The adjacency + recursion:
158+
159+
$$
160+
S(V, E) = O(V + E)
161+
$$
162+
163+
## Variants & follow-ups
164+
165+
- **Reconstruct Itinerary** ([17.9](../ch17-advanced-graphs/reconstruct-itinerary.md)) — the lexicographic member of the family, with the min-heap twist.
166+
- **Theory.kt** (`graph/euler/circuit/Theory.kt`) — the repo's own prose on Eulerian conditions.
167+
- **Interview follow-up:** "Why is post-order essential in all three?" A greedy pre-order would dead-end on a bridge-like edge (consume a cut edge early, strand the rest). Post-order appends a node/edge *after* its subtree is consumed — the deferral is what lets the DFS backtrack into remaining edges automatically. All three files are that one idea, three costumes.

0 commit comments

Comments
 (0)