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