Advanced Graphs: Euler Paths, Bridges & SCCs
Use every edge once, find the single points of failure, and group what can reach itself.
▶ guided lesson⚡ 1-minute summary↻ 15 recall cards
before this: Graphs · Heaps · Union-Find & MST · Topo Sort
01 · why it exists
Why this exists, and the weight it carries
since1736; 1873
Euler paths and Hierholzer's algorithm
Leonhard Euler; Carl Hierholzer (reconstructed by Christian Wiener)
Euler's Königsberg paper (1736 — see the Graphs chapter) gave the degree rule for walking every bridge once, but did not prove the rule was enough. Carl Hierholzer, a lecturer in Karlsruhe, found the proof and its method — walk until stuck, then splice in a loop from any vertex with unused edges — and presented it to fellow mathematicians. He died in September 1871 without writing it down. Christian Wiener reconstructed it from memory, with Jacob Lüroth's help, and published it in Mathematische Annalen in 1873. In 1883 Fleury published a slower rule: never cross a bridge of the unused edges unless you must. In 2001 Pevzner, Tang and Waterman's EULER assembler made Euler paths in de Bruijn graphs practical for DNA, sidestepping the NP-complete Hamiltonian model.
the problem it solvedProving exactly when a walk that uses every edge once exists — and building one in time proportional to the number of edges.
its jobs today
- Genome assembly: short-read assemblers such as Velvet (2008) build a de Bruijn graph of overlapping DNA pieces (the Euler-path model) and read long stretches of genome off paths through it.
- Covering every street once: garbage-collection and road-inspection routes are Euler circuits; the Chinese postman problem adds repeated streets of the least total length so one exists.
- Testing every combination: a de Bruijn sequence (an Euler circuit) types every 4-digit PIN in 10⁴ + 3 = 10,003 key presses instead of 4 × 10⁴ = 40,000 — LC 753 Cracking the Safe.
the weight it carries
Illustrative: LC 332 at its limit, 300 tickets. Hierholzer: 300 heap polls of about ⌈log₂ 300⌉ = 9 comparisons ≈ 2,700 steps. Trying ticket orders blindly could mean up to 300! orders. At genome scale, assemblers handle millions of overlaps in linear time where the vertex-based (Hamiltonian) version has no known fast algorithm.
without it
"Use every link exactly once" would be a search over orders — hopeless beyond a few dozen edges — and DNA assembly would be stuck with an NP-complete model.
when · why · how
- when
- Use every edge (ticket, domino, pair, street) exactly once, or build one sequence that covers all overlaps.
- why
- Degrees decide existence in O(V + E). Hierholzer then walks each edge once: O(E), or O(E log E) with min-heaps when the smallest route is required.
- how
Map<String, PriorityQueue<String>>for named vertices (aused[]pointer for int vertices); a recursive post-ordervisitor anArrayDequestack; add a vertex when stuck,Collections.reverseat the end.
since1972–1974
Bridges and articulation points (low-link DFS)
Robert Tarjan; John Hopcroft & Robert Tarjan
Tarjan's 1972 paper "Depth-First Search and Linear Graph Algorithms" showed that one DFS, with a "lowpoint" number per vertex, finds a graph's biconnected components — and so its articulation points — in time proportional to V + E. The biconnectivity method was joint work with John Hopcroft; their "Algorithm 447" in Communications of the ACM (1973) published it. Bridges fall out of the same idea, and in 1974 Tarjan also published a short note with a linear-time algorithm just for bridges. The obvious alternative — delete each edge and re-check connectivity — costs O(E · (V + E)).
the problem it solvedFinding every edge or vertex whose removal splits a graph, with a single DFS instead of one search per edge.
its jobs today
- Network reliability: separation edges and vertices are the single points of failure — a cable or router whose loss cuts the network in two.
- Planning redundancy: every bridge is a link with no backup path; add links until none is left and any one link can fail safely.
- Map chokepoints: in a road or game-map graph, a bridge is the one corridor whose blocking splits the map.
the weight it carries
Illustrative: 10⁴ routers and 5 × 10⁴ cables. Remove each cable and BFS: 5 × 10⁴ × (10⁴ + 5 × 10⁴) = 3 × 10⁹ steps. One low-link DFS: 10⁴ + 5 × 10⁴ = 6 × 10⁴ steps — 50,000× less (one search instead of one per cable).
without it
Finding single points of failure would mean re-checking connectivity once per link: billions of steps on a mid-sized network instead of one pass.
when · why · how
- when
- "Critical connections", "single point of failure", "which edge or node disconnects the graph".
- why
- tin[] records arrival order; low[] the highest point a subtree can climb back to. A tree edge u–v is a bridge iff low[v] > tin[u]; a non-root u is a cut vertex iff low[v] ≥ tin[u]. One DFS: O(V + E).
- how
- Two
int[]s (tin filled with -1) and a recursivedfs(u, parent)that skips the parent;Math.minwithtin[v]on back edges and withlow[v]after a child. Switch to anArrayDequestack for 10⁵-deep inputs.
since1972; 1978; 1981
Strongly connected components (Kosaraju, Tarjan)
Robert Tarjan; S. Rao Kosaraju; Micha Sharir
In a directed graph, u and v share a strongly connected component when each can reach the other. Tarjan's 1972 paper found every SCC in one DFS, using "lowlink" numbers and a stack — in linear time. A simpler two-pass method is credited to S. Rao Kosaraju, who found it in 1978 but never published it; Micha Sharir discovered it independently and published it in 1981. Aho, Hopcroft and Ullman's 1983 textbook credits it to both.
the problem it solvedGrouping a directed graph into maximal "everyone reaches everyone" clusters in linear time, so the rest of the graph becomes a DAG.
its jobs today
- Compilers: LLVM's CallGraphSCCPass (legacy pass manager) traverses the call graph bottom-up, callees before callers, one SCC (a group of mutually recursive functions) at a time.
- 2-SAT solvers: a formula is unsatisfiable exactly when some x and NOT x land in the same SCC of its implication graph (Aspvall, Plass & Tarjan, 1979).
- Measuring networks: Broder et al.'s 1999 web crawl of 203 million pages found one giant SCC of about 56 million pages.
- Dependency tools: collapse each circular group of modules into one node, and what's left can be topologically sorted.
the weight it carries
Illustrative: 10⁵ functions and 5 × 10⁵ call edges. Asking "does f reach g and g reach f?" for every pair: 10⁵ × (10⁵ − 1) / 2 ≈ 5 × 10⁹ questions, each a search. Kosaraju: 2 passes × (10⁵ + 5 × 10⁵) = 1.2 × 10⁶ steps.
without it
Cycles would block topological order with no way to group them, and mutual reachability would need a search per pair of vertices.
when · why · how
- when
- A directed graph where the question is mutual reachability: groups that all reach each other, collapsing cycles into single nodes, 2-SAT.
- why
- Kosaraju: finish order on the graph, then DFS on the reversed graph in reverse finish order — each DFS is trapped inside one SCC. Tarjan: one DFS with low-links and a stack. Both O(V + E).
- how
- Kosaraju: two
List<List<Integer>>(graph and reversed), anArrayDeque<Integer>of finished vertices andint[] comp. Tarjan:tin[],low[],boolean[] onStackand anArrayDequestack.
since?
Ratios as a weighted graph (and weighted union-find)
No single inventor: multiplying conversion factors along a chain is ordinary unit arithmetic. Union-find that also stores each element's ratio to its parent is a widely taught extension of Galler and Fischer's 1964 structure (see the Union-Find chapter); we found no single published origin for it.
its jobs today
- Unit and currency conversion: store a few known rates and multiply along a chain — km → m → cm, or any currency through a base currency.
- Consistency checks: the weighted union-find spots a contradiction the moment a new fact disagrees with a chain of old ones.
- Arbitrage detection: with −log(rate) as the weight, a cycle of exchange rates whose product exceeds 1 becomes a negative cycle for Bellman–Ford (Shortest Paths chapter).
the weight it carries
Illustrative: 10⁴ variables, 10⁴ facts and 10⁴ queries. DFS per query: up to 10⁴ × (10⁴ + 2 × 10⁴) = 3 × 10⁸ steps. Weighted union-find: about 2 × 10⁴ near-constant operations. At LC 399's limits both are instant: at most 20 × (40 + 40) = 1,600 DFS steps.
without it
Every derived ratio would have to be listed in advance (V² of them), or worked out by trial and error.
when · why · how
- when
- Facts like
a / b = k, conversion factors, "x is k times y" — and questions about pairs that were never given directly. - why
- An edge a → b with weight a/b (plus b → a with b/a) turns a query into a path, and any path multiplies to the same ratio. DFS per query: O(Q · (V + E)); weighted union-find: near-constant per query after building.
- how
Map<String, Map<String, Double>>built withcomputeIfAbsent; a DFS carrying the running product and aHashSetof seen variables. Union-find:parentandweightmaps where weight(x) = x / parent(x).
since2012
BFS over a derived graph (choose the vertices)
Daniel Delling, Thomas Pajor & Renato Werneck (Microsoft Research)
Picking the right vertices is an old modelling habit with no single inventor, but transit routing shows it at full scale. Journey planners must answer "fewest changes" as well as "earliest arrival". In 2012 Delling, Pajor and Werneck published RAPTOR, which works in rounds: round k finds the fastest way to every stop using at most k trips, scanning each route at most once per round. That is our Bus Routes BFS, grown up: one level is one more vehicle, not one more stop.
the problem it solvedAnswering fewest-transfer and earliest-arrival journey queries on city-scale transit networks quickly, without Dijkstra.
its jobs today
- Metro and bus journey planners minimise changes: a round is one more vehicle, not one more stop.
- Puzzle BFS (Word Ladder, gene mutation, lock combinations): choose states so that one edge costs exactly one move.
- Group-based BFS: when items share a group (a route, a value in Jump Game IV), expand each group once instead of linking every pair inside it.
the weight it carries
Illustrative: one route with 10⁵ stops (LC 815 allows it). Stop graph: every pair of its stops is an edge — 10⁵ × (10⁵ − 1) / 2 ≈ 5 × 10⁹ edges. Bus graph: each of the 10⁵ stop entries is scanned once — 10⁵ steps.
without it
The obvious stop-to-stop graph has a clique per route: billions of edges for a single long route, so the BFS never even finishes building.
when · why · how
- when
- The cost counts groups (buses, flights, levels, colours), not single steps — or the obvious graph would have a clique per group.
- why
- Make the groups the vertices (or expand each group once): each group is queued once and each membership scanned once — O(Σ group sizes).
- how
Map<Integer, List<Integer>>stop → buses, aboolean[]of boarded buses, aHashSetof seen stops, and anArrayDequeprocessed level by level withqueue.size().
02 · plain english
The idea in 30 seconds
BFS, DFS, shortest paths, MST and topological sort answer reach, distance, cost and order. This chapter asks new questions with the same DFS/BFS skeleton.
Use every edge once? Euler path (Hierholzer). Which link is a single point of failure? Bridges (low-links). Who can reach whom both ways? Strongly connected components (Kosaraju).
Plus two modelling moves: ratios are weighted edges (multiply along a path), and choose the right vertices (in Bus Routes, a bus is a vertex).
picture this
Picture your company's microservice map. An Euler path is one test run that calls every API link exactly once. A bridge is the one link whose failure cuts a group of services off — a single point of failure. An SCC is a cluster of services that all end up calling each other, so they must be deployed and debugged as one unit.
03 · remember
What you must know
- Euler path = a walk that uses every EDGE exactly once. Directed: all edges connected, and every vertex has out = in, except one start (out − in = 1) and one end (in − out = 1). Undirected: 0 or 2 odd-degree vertices.
- Hierholzer: walk until you are stuck; when a vertex has no unused edges left, add it to the route; reverse at the end. Dead ends land at the end of the route by themselves, and loops get spliced in. O(E) — O(E log E) with a min-heap per vertex.
- Euler ≠ Hamiltonian. Visiting every VERTEX once (Hamiltonian) is NP-complete; using every EDGE once (Euler) is linear. Recasting a problem in Euler form is a superpower — it's how short-read DNA assemblers work.
- tin and low: DFS stamps
tin[u]when it first reaches u;low[u]= the smallesttinthat u's subtree can reach with one back edge. A tree edge u–v is a bridge ifflow[v] > tin[u]. - Articulation point (cut vertex): a non-root u with a DFS child v where
low[v] ≥ tin[u]; the DFS root iff it has 2+ DFS children. In both tests, skip the edge you came in on. - SCC = a largest group of vertices that all reach each other (directed). Shrink every SCC to one node and the graph becomes a DAG — so topological sort works again.
- Kosaraju: pass 1 records the DFS finish order; pass 2 runs DFS on the REVERSED graph in reverse finish order, and each DFS collects exactly one SCC. Tarjan: one DFS with low-links and a stack. Both O(V + E).
- Ratios are a weighted graph:
a / b = kgives a → b with weight k and b → a with 1/k. A query is a path: multiply the weights. A variable that never appears → -1. - Choose the vertices: when the cost counts groups (buses, flights), make the groups the vertices. Bus Routes as BFS over buses is O(Σ route lengths); over stops a single long route can mean ~5·10⁹ edges.
- Java depth: these are recursive DFS algorithms. A 10⁵-vertex chain can overflow the call stack — know the explicit-stack version (
ArrayDeque+ a per-vertex neighbour index).
04 · see it
Play with it in 3D
Press Space to play, ←/→ to step, drag to move, ? for all shortcuts. Try your own input.
Hierholzer — rebuild the itinerary
Each airport keeps a min-heap of unused tickets. An airport joins the route only when it is stuck — so BLR, a dead end, lands at the end of the trip.
Bridges with tin and low (Tarjan)
Every vertex shows tin · low. Dashed edges are back edges (a second way up). An edge turns red when low[child] > tin[parent].
Strongly connected components — Kosaraju's two passes
Pass 1 fills the finished stack. Pass 2 flips every arrow and pops the stack: each DFS is trapped inside one component.
Ratios as a weighted graph — Evaluate Division
Edge a → b carries a / b. A query walks from c to d multiplying the weights; the product so far sits under each vertex on the path.
Choosing the vertices — Bus Routes
Boxes are buses (the BFS vertices), spheres are stops. One BFS level = one more bus boarded.
05 · cost
Time & space, explained
| Time | Extra space | why | |
|---|---|---|---|
| Euler degree check | O(V + E) | O(V) | One pass over the edges counts each vertex's in- and out-degree. |
| Hierholzer (pointer per vertex) | O(V + E) | O(V + E) | Each edge is walked once; used[u] skips edges already taken, so nothing is scanned twice. |
| Hierholzer + min-heaps (LC 332) | O(E log E) | O(E) | Each ticket is offered to and polled from a heap once; each heap operation is O(log E). |
| Backtracking itinerary | exponential (worst case) | O(E) | A wrong early ticket may fail only much later; undoing and retrying can explore a huge number of orders. |
| Fleury (naive bridge checks) | O(E · (V + E)) | O(V + E) | Before each of the E steps it asks "is this edge a bridge of the unused graph?" — a full search each time. |
| Bridges / cut vertices (low-link DFS) | O(V + E) | O(V + E) | One DFS: each vertex is entered once, each adjacency entry looked at once. Adjacency list plus tin, low and up to V stack frames. |
| Remove each edge + re-check connectivity | O(E · (V + E)) | O(V + E) | E separate BFS runs, each O(V + E). |
| Kosaraju SCC | O(V + E) | O(V + E) | Two DFS passes. The extra O(E) is the reversed copy of the graph that pass 2 walks. |
| Tarjan SCC | O(V + E) | O(V) beyond the graph | One DFS; each vertex is pushed on and popped off the stack once. No reversed copy — just tin, low, onStack and the stack. |
| Time | Extra space | why | |
|---|---|---|---|
| Evaluate Division: DFS per query | O(Q · (V + E)) | O(V + E) | Each of the Q walks may visit every variable and edge once. |
| Evaluate Division: all-pairs closure | O(V³ + Q) | O(V²) | Every middle variable is tried for every pair (V³); then each query is a table lookup. |
| Evaluate Division: weighted union-find | O((E + Q) · log V) amortized | O(V) | Path compression alone gives amortized O(log V) per find (α(V) with union by size too); map lookups are O(1) on average. |
| Bus Routes: BFS over stops | O(Σ Lᵢ²) | O(Σ Lᵢ²) | A route with L stops joins every pair of them: L · (L − 1) directed edges per route. |
| Bus Routes: BFS over buses | O(Σ Lᵢ) | O(Σ Lᵢ) | The stop → buses index reads each stop entry once; each bus is polled once and each stop's bus list is scanned once. |
06 · java
Raw vs Collections Framework
Euler path: check the degrees, then Hierholzer
/** Directed graph on 0..n−1. Returns the vertex an Euler path must start from, or -1 if none can exist. */static int startVertex(int n, int[][] edges) { int[] out = new int[n], in = new int[n]; for (int[] e : edges) { out[e[0]]++; in[e[1]]++; } int start = -1, starts = 0, ends = 0; for (int v = 0; v < n; v++) { int diff = out[v] - in[v]; if (diff == 1) { starts++; start = v; // one more edge out than in: the path must begin here } else if (diff == -1) { ends++; // one more edge in than out: the path ends here } else if (diff != 0) { return -1; // off by 2 or more: impossible } } if (starts == 1 && ends == 1) return start; // an Euler PATH if (starts > 0 || ends > 0) return -1; for (int v = 0; v < n; v++) if (out[v] > 0) return v; // an Euler CIRCUIT: start anywhere with an edge return 0; // no edges at all}/** An Euler path as a list of vertices, or an empty list if the edges can't be walked in one go. */static List<Integer> eulerPath(int n, int[][] edges) { int start = startVertex(n, edges); if (start == -1) return List.of(); List<List<Integer>> adj = new ArrayList<>(); for (int v = 0; v < n; v++) adj.add(new ArrayList<>()); for (int[] e : edges) adj.get(e[0]).add(e[1]); int[] used = new int[n]; // used[u] = how many of u's edges are already walked Deque<Integer> stack = new ArrayDeque<>(List.of(start)); List<Integer> path = new ArrayList<>(); while (!stack.isEmpty()) { int u = stack.peek(); if (used[u] < adj.get(u).size()) { stack.push(adj.get(u).get(used[u]++)); // walk an unused edge } else { path.add(stack.pop()); // stuck: u is final, counted from the end } } Collections.reverse(path); return path.size() == edges.length + 1 ? path : List.of(); // fewer → some edges were unreachable}- The degree check is necessary but not enough: the edges must also be connected. Hierholzer tells you for free — if the path has fewer than E + 1 vertices, some edges were unreachable.
used[u]is a pointer into u's list: edge numberused[u]is the next unused one. Noremovecalls, so every step is O(1).- Push while there is an unused edge; pop into
pathwhen stuck. The popped order is the trip backwards —Collections.reverseonce at the end. - Undirected version: start at an odd-degree vertex if there is one, and mark each edge used (by edge id) so it isn't walked from both ends.
Bridges and articulation points: tin[] and low[]
class BridgeFinder { private List<List<Integer>> adj; private int[] tin, low; // discovery time · lowest time the subtree can reach with one back edge private int timer; private final List<int[]> bridges = new ArrayList<>(); /** Undirected graph on 0..n−1, no parallel edges. Works on disconnected graphs too. */ List<int[]> find(int n, int[][] edges) { adj = new ArrayList<>(); for (int v = 0; v < n; v++) adj.add(new ArrayList<>()); for (int[] e : edges) { adj.get(e[0]).add(e[1]); adj.get(e[1]).add(e[0]); } tin = new int[n]; low = new int[n]; Arrays.fill(tin, -1); for (int s = 0; s < n; s++) if (tin[s] == -1) dfs(s, -1); return bridges; } private void dfs(int u, int parent) { tin[u] = low[u] = timer++; for (int v : adj.get(u)) { if (v == parent) continue; // the edge we came in on doesn't count if (tin[v] != -1) { low[u] = Math.min(low[u], tin[v]); // back edge: a second way up } else { dfs(v, u); low[u] = Math.min(low[u], low[v]); if (low[v] > tin[u]) bridges.add(new int[] {u, v}); // v's side can't reach u or above } } }}class CutVertexFinder { private List<List<Integer>> adj; private int[] tin, low; private boolean[] cut; private int timer; /** Vertices whose removal splits their connected component, in increasing order. */ List<Integer> find(int n, int[][] edges) { adj = new ArrayList<>(); for (int v = 0; v < n; v++) adj.add(new ArrayList<>()); for (int[] e : edges) { adj.get(e[0]).add(e[1]); adj.get(e[1]).add(e[0]); } tin = new int[n]; low = new int[n]; cut = new boolean[n]; Arrays.fill(tin, -1); for (int s = 0; s < n; s++) if (tin[s] == -1) dfs(s, -1); List<Integer> out = new ArrayList<>(); for (int v = 0; v < n; v++) if (cut[v]) out.add(v); return out; } private void dfs(int u, int parent) { tin[u] = low[u] = timer++; int children = 0; for (int v : adj.get(u)) { if (v == parent) continue; if (tin[v] != -1) { low[u] = Math.min(low[u], tin[v]); } else { dfs(v, u); children++; low[u] = Math.min(low[u], low[v]); if (parent != -1 && low[v] >= tin[u]) cut[u] = true; // v's side can't get above u } } if (parent == -1 && children > 1) cut[u] = true; // a root is a cut vertex iff it has 2+ DFS children }}- Fill
tinwith -1 so it doubles as the visited check.low[u]starts equal totin[u]. - Back edge (already visited, not the parent):
low[u] = Math.min(low[u], tin[v]). After a child returns:low[u] = Math.min(low[u], low[v]). - Bridge:
low[v] > tin[u](strict). Cut vertex:low[v] >= tin[u]for a non-root u, and the root needs 2+ DFS children. - Skipping the parent vertex is right when there are no parallel edges (LC 1192 promises none). With repeated cables, skip the parent edge id instead.
- Loop over all vertices as starts if the graph may be disconnected —
Bridges.javadoes; LC 1192's network is connected, so one DFS from 0 is enough there.
Deep graphs in Java: recursion vs an explicit stack
class Solution { private List<List<Integer>> adj; private int[] tin, low; // discovery time · lowest time the subtree can reach with one back edge private int timer; private List<List<Integer>> bridges; public List<List<Integer>> criticalConnections(int n, List<List<Integer>> connections) { adj = new ArrayList<>(); for (int i = 0; i < n; i++) adj.add(new ArrayList<>()); for (List<Integer> c : connections) { adj.get(c.get(0)).add(c.get(1)); adj.get(c.get(1)).add(c.get(0)); } tin = new int[n]; low = new int[n]; Arrays.fill(tin, -1); // -1 = not visited yet timer = 0; bridges = new ArrayList<>(); dfs(0, -1); // the network is connected, so one DFS reaches every server return bridges; } private void dfs(int u, int parent) { tin[u] = low[u] = timer++; for (int v : adj.get(u)) { if (v == parent) continue; // the edge we came in on doesn't count if (tin[v] != -1) { low[u] = Math.min(low[u], tin[v]); // back edge: a second way up } else { dfs(v, u); // tree edge: explore v's side first low[u] = Math.min(low[u], low[v]); if (low[v] > tin[u]) bridges.add(List.of(u, v)); // v's side can't reach u or above } } }}class Solution { /** The same low-link DFS with an explicit stack: no StackOverflowError on a 10⁵-server chain. */ public List<List<Integer>> criticalConnections(int n, List<List<Integer>> connections) { List<List<Integer>> adj = new ArrayList<>(); for (int i = 0; i < n; i++) adj.add(new ArrayList<>()); for (List<Integer> c : connections) { adj.get(c.get(0)).add(c.get(1)); adj.get(c.get(1)).add(c.get(0)); } int[] tin = new int[n], low = new int[n], parent = new int[n]; int[] next = new int[n]; // next[u] = index of u's next neighbour to look at Arrays.fill(tin, -1); List<List<Integer>> bridges = new ArrayList<>(); Deque<Integer> stack = new ArrayDeque<>(List.of(0)); int timer = 0; tin[0] = low[0] = timer++; parent[0] = -1; while (!stack.isEmpty()) { int u = stack.peek(); if (next[u] < adj.get(u).size()) { int v = adj.get(u).get(next[u]++); if (v == parent[u]) continue; if (tin[v] != -1) { low[u] = Math.min(low[u], tin[v]); // back edge } else { tin[v] = low[v] = timer++; // tree edge: "call" v parent[v] = u; stack.push(v); } } else { stack.pop(); // u is finished: "return" to its parent int p = parent[u]; if (p == -1) continue; low[p] = Math.min(low[p], low[u]); if (low[u] > tin[p]) bridges.add(List.of(p, u)); } } return bridges; }}- Each recursive call is a stack frame. A 10⁵-server chain needs 10⁵ nested calls, and the default thread stack may overflow (
StackOverflowError). - The iterative version keeps
next[u], the index of u's next neighbour to try.peekthe top: if it has a neighbour left, handle it (maybepusha child); otherwisepopand update the parent. - "Returning" from a child is the moment you
popit: that's wherelow[parent] = min(low[parent], low[child])and the bridge test go. - In an interview, write the recursive version (clearer), then say the depth risk and how you'd convert it.
Strongly connected components: Tarjan vs Kosaraju
class TarjanScc { private List<List<Integer>> adj; private int[] tin, low, comp; private boolean[] onStack; private final Deque<Integer> stack = new ArrayDeque<>(); private int timer, count; /** comp[v] = id of v's component. Ids come out in REVERSE topological order (sinks first). */ int[] components(int n, int[][] edges) { adj = new ArrayList<>(); for (int v = 0; v < n; v++) adj.add(new ArrayList<>()); for (int[] e : edges) adj.get(e[0]).add(e[1]); tin = new int[n]; low = new int[n]; comp = new int[n]; onStack = new boolean[n]; Arrays.fill(tin, -1); for (int v = 0; v < n; v++) if (tin[v] == -1) dfs(v); return comp; } private void dfs(int u) { tin[u] = low[u] = timer++; stack.push(u); onStack[u] = true; for (int v : adj.get(u)) { if (tin[v] == -1) { dfs(v); low[u] = Math.min(low[u], low[v]); } else if (onStack[v]) { low[u] = Math.min(low[u], tin[v]); // v is in the component still being built } } if (low[u] == tin[u]) { // nothing below u reaches above it: u roots a component int v; do { v = stack.pop(); onStack[v] = false; comp[v] = count; } while (v != u); count++; } }}class Kosaraju { /** comp[v] = id of v's strongly connected component. Ids follow a topological order of the components. */ int[] components(int n, int[][] edges) { List<List<Integer>> adj = new ArrayList<>(), reversed = new ArrayList<>(); for (int v = 0; v < n; v++) { adj.add(new ArrayList<>()); reversed.add(new ArrayList<>()); } for (int[] e : edges) { adj.get(e[0]).add(e[1]); reversed.get(e[1]).add(e[0]); } boolean[] seen = new boolean[n]; Deque<Integer> finished = new ArrayDeque<>(); for (int v = 0; v < n; v++) { if (!seen[v]) finishOrder(v, adj, seen, finished); // pass 1 } int[] comp = new int[n]; Arrays.fill(comp, -1); int count = 0; while (!finished.isEmpty()) { int v = finished.pop(); // latest finisher first if (comp[v] == -1) collect(v, count++, reversed, comp); // pass 2, on reversed edges } return comp; } private void finishOrder(int u, List<List<Integer>> adj, boolean[] seen, Deque<Integer> finished) { seen[u] = true; for (int v : adj.get(u)) { if (!seen[v]) finishOrder(v, adj, seen, finished); } finished.push(u); // u finishes after everything it reaches } private void collect(int u, int id, List<List<Integer>> reversed, int[] comp) { comp[u] = id; for (int v : reversed.get(u)) { if (comp[v] == -1) collect(v, id, reversed, comp); } }}- Kosaraju needs a reversed adjacency list — build it in the same loop:
reversed.get(e[1]).add(e[0]). - Pass 1 pushes a vertex on an
ArrayDequewhen it finishes (the same finish order as topological sort). Pass 2 pops: latest finisher first. - Tarjan:
onStack[v]matters — an edge into a vertex of an already-finished component must not lowerlow[u]. - Kosaraju's ids come out in topological order of the components; Tarjan's in reverse (sinks first).
Ratios: a map of maps vs weighted union-find
class Solution { private Map<String, String> parent; private Map<String, Double> weight; // weight.get(x) = x / parent(x) public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) { parent = new HashMap<>(); weight = new HashMap<>(); for (int i = 0; i < equations.size(); i++) union(equations.get(i).get(0), equations.get(i).get(1), values[i]); double[] answers = new double[queries.size()]; for (int q = 0; q < queries.size(); q++) { String c = queries.get(q).get(0), d = queries.get(q).get(1); if (!parent.containsKey(c) || !parent.containsKey(d) || !find(c).equals(find(d))) answers[q] = -1.0; else answers[q] = weight.get(c) / weight.get(d); // (c / root) / (d / root) = c / d } return answers; } /** Returns x's root and flattens the path; afterwards weight(x) = x / root. */ private String find(String x) { String p = parent.get(x); if (p.equals(x)) return x; String root = find(p); weight.put(x, weight.get(x) * weight.get(p)); // (x / p) * (p / root) parent.put(x, root); return root; } /** Records a / b = value by hanging a's root under b's root. */ private void union(String a, String b, double value) { for (String s : List.of(a, b)) { parent.putIfAbsent(s, s); weight.putIfAbsent(s, 1.0); } String ra = find(a), rb = find(b); if (ra.equals(rb)) return; parent.put(ra, rb); weight.put(ra, value * weight.get(b) / weight.get(a)); // ra / rb = (a / b) * (b / rb) / (a / ra) }}class Solution { public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) { Map<String, Map<String, Double>> graph = new HashMap<>(); // graph.get(a).get(b) = a / b for (int i = 0; i < equations.size(); i++) { String a = equations.get(i).get(0), b = equations.get(i).get(1); graph.computeIfAbsent(a, k -> new HashMap<>()).put(b, values[i]); graph.computeIfAbsent(b, k -> new HashMap<>()).put(a, 1.0 / values[i]); } double[] answers = new double[queries.size()]; for (int q = 0; q < queries.size(); q++) { String c = queries.get(q).get(0), d = queries.get(q).get(1); if (!graph.containsKey(c) || !graph.containsKey(d)) answers[q] = -1.0; // never seen else answers[q] = walk(c, d, 1.0, graph, new HashSet<>()); } return answers; } /** product = start / cur so far. Returns start / target, or -1 if target can't be reached. */ private double walk(String cur, String target, double product, Map<String, Map<String, Double>> graph, Set<String> seen) { if (cur.equals(target)) return product; seen.add(cur); for (Map.Entry<String, Double> edge : graph.get(cur).entrySet()) { if (seen.contains(edge.getKey())) continue; double result = walk(edge.getKey(), target, product * edge.getValue(), graph, seen); if (result != -1.0) return result; } return -1.0; // dead end: back up }}graph.computeIfAbsent(a, k -> new HashMap<>()).put(b, value)— and the reverse edge with1.0 / value. Forgetting the reverse is the #1 bug.HashMapvisits keys in hash order, not insertion order. The answer doesn't depend on it (any path gives the same ratio), only the walk order does.- In weighted union-find,
weight(x) = x / parent(x).findmultiplies the weights on the way up while it flattens the path. - Doubles: our judge accepts answers within 10⁻⁵, so multiplying along a different path (or in a different order) is fine.
| call | does | cost |
|---|---|---|
new PriorityQueue<String>() | Min-heap of destinations: poll() gives the lexicographically smallest | O(log n) offer/poll |
map.computeIfAbsent(k, x -> new PriorityQueue<>()).offer(v) | Adjacency with sorted neighbours for string vertices | O(1) average + O(log n) |
Collections.reverse(list) | Turn the post-order route into the real trip | O(n) |
deque.push(x) / deque.peek() / deque.pop() | Explicit DFS stack instead of recursion | O(1) |
Arrays.fill(tin, -1) | Mark every vertex unvisited before a low-link DFS | O(n) |
Math.min(low[u], low[v]) | Pull a child's reach up into its parent | O(1) |
List.of(u, v) | An immutable pair for the answer list | O(1) |
map.getOrDefault(stop, List.of()) | Buses at a stop, or an empty list for a stop no bus visits | O(1) average |
Use every edge once, smallest order (LC 332)
→ Hierholzer + a PriorityQueue per vertex
Post-order handles dead ends; the heaps give the smallest route. O(E log E).
Use every edge once, any order, int vertices
→ Hierholzer with a used[] pointer per vertex
O(V + E), no heap, no removals.
Edges whose removal disconnects the graph
→ Low-link DFS: low[v] > tin[u]
One DFS, O(V + E), instead of E connectivity checks.
Vertices whose removal disconnects the graph
→ Same DFS with low[v] >= tin[u] + the root rule
Equality saves an edge but not a vertex.
SCCs with the least risk of bugs
→ Kosaraju (two passes)
Two plain DFS runs; reuses the finish-order idea from topological sort.
SCCs in one pass, less memory
→ Tarjan's SCC
No reversed graph; one DFS with a stack.
A few ratio queries (LC 399 limits)
→ DFS per query on a map of maps
Simplest code; 20 queries × tiny graph is instant.
Many ratio queries on fixed equations
→ Weighted union-find
Near-constant time per query after building.
Fewest buses / flights / transfers
→ BFS where each group is a vertex
One level = one more group; avoids a clique per group.
07 · recognise
Spot the pattern
"Use every ticket / edge / domino exactly once"
Euler path: degree check + Hierholzer (add when stuck, reverse)
"Critical connection", "single point of failure", "removing one link disconnects"
Bridges with a tin/low DFS (articulation points if it's a node)
Directed graph: "groups that can all reach each other", "collapse the cycles"
SCCs (Kosaraju or Tarjan), then work on the condensation DAG
"a / b = k", conversion factors, "x is k times y"
Weighted graph: multiply along a path (or weighted union-find)
"Fewest buses / flights / transfers" — the cost counts groups, not single steps
BFS where the groups are the vertices
Rebuild one sequence from overlapping pieces (pairs that chain, de Bruijn)
Euler path over the overlaps
08 · traps & tricks
Mistakes to avoid, tricks to use
traps
- LC 332: taking the smallest ticket greedily and stopping at a dead end, or adding an airport when you arrive (pre-order). Add it when it's stuck (post-order) and reverse at the end.
- LC 332: removing from the front of an
ArrayListfor every ticket is O(E) each. Use aPriorityQueue(or a sortedArrayDeque) per airport. - Bridges: forgetting to skip the parent edge. The parent then looks like a back edge, every
low[v] ≤ tin[u], and no bridge is ever found. - Articulation points: reusing the bridge test
>instead of≥, or forgetting the root rule (2+ DFS children). - Kosaraju: running pass 2 on the original graph, or in plain vertex order — components leak into each other.
- Evaluate Division: returning 1.0 for
x / xwhen x appears in no equation (it must be -1.0), or forgetting the reverse edge b → a = 1 / value. - Bus Routes: a stop-to-stop graph with a clique per route (Time Limit Exceeded on a long route), and forgetting
source == target → 0. - Recursive DFS on 10⁵ vertices in Java: a long chain can throw
StackOverflowError. Know the explicit-stack version.
tricks
- Name the question first: "every edge once" = Euler, "one link splits it" = bridge, "reach each other" = SCC. The name picks the algorithm.
- Chalo, dry-run Hierholzer on the 3-ticket dead end (JFK→KUL, JFK→NRT, NRT→JFK) before you code. It takes a minute and catches the "stop at the first dead end" bug.
- For bridges, write
tin/lownext to each vertex while you trace. Say it out loud: "low is the highest point this subtree can climb back to." - Mention recursion depth for n = 10⁵ in Java and offer the iterative version — interviewers like hearing the trade-off, even if you code the recursive one.
- Bus Routes: say "the cost is buses, so buses are my vertices". It shows you design the graph, not just traverse it.
- Bas itna yaad rakho for ratios: store both directions (a/b and b/a), multiply along the path, unknown variable → -1.
09 · why & how
Questions you might be too shy to ask
Euler path vs Hamiltonian path — what's the difference?
Euler uses every edge exactly once; Hamiltonian visits every vertex exactly once. Euler is easy: a degree check plus Hierholzer, O(E). Hamiltonian is NP-complete — no known fast algorithm. That gap is why short-read DNA assemblers make each small piece an edge (joining its overlapping ends) and look for an Euler path, instead of making each piece a vertex.
Why must every middle vertex have in-degree = out-degree for an Euler path?
Each time the walk passes through a vertex, it uses one edge in and one edge out — so a middle vertex always spends its edges in in/out pairs. Only the start gets one extra "out" (you leave without arriving first), and only the end gets one extra "in". Undirected is the same idea: passing through uses 2 edges, so middle vertices need an even degree.
Where should Hierholzer start?
Directed: at the vertex with out − in = 1. If every vertex is balanced, the walk is a circuit — start at any vertex that has an edge, and you finish where you began. Undirected: at one of the two odd-degree vertices, or anywhere if all degrees are even. LC 332 makes it easy: the trip always starts at JFK.
Why does "always fly to the smallest airport" fail, but Hierholzer with the same heaps works?
Greedy walks into a dead end (JFK → KUL) while other tickets are still unused, and has no way back. Hierholzer backs up along its stack and keeps walking from the first airport that still has tickets: the dead end gets added to the route first — so it ends up LAST after the reverse — and the remaining loop (JFK → NRT → JFK) is spliced in before it.
Tree edge vs back edge — how do I tell them apart in the DFS?
From u, look at a neighbour v. If v is unvisited (tin[v] == -1), you walk down to it: that's a tree edge. If v is already visited and isn't the parent you came from, it's a back edge: in an undirected DFS it always joins u to an ancestor or a descendant, a second route along the tree. Back edges make cycles, and a cycle is what saves an edge from being a bridge.
What exactly is low[u]?
The smallest discovery time tin that u, or anything below u in the DFS tree, can reach using tree edges down and at most one back edge up. If a child v has low[v] > tin[u], nothing under v can climb back to u or above — so the edge u–v is the only link, a bridge.
Why does a back edge update with tin[v], but a child with low[v]?
A back edge is one jump: it lands exactly on v, so the highest point it reaches is tin[v]. A child is different: low[v] already sums up everything v's whole subtree can reach, and u can use all of it because u's tree edge leads down into that subtree. Stick to this pair — it matches the definition "tree edges down, then at most one back edge up".
Why > for bridges but ≥ for articulation points?
low[v] == tin[u] means some back edge from v's side lands exactly on u. Remove the edge u–v and v still reaches u through that back edge — not a bridge. Remove the vertex u and the landing spot is gone too — so u is a cut vertex. Equality saves the edge, not the vertex.
One exception: the DFS root has no parent side, so it is a cut vertex only if it has 2 or more DFS children.
Why skip the parent, and what about two cables between the same servers?
In an undirected graph, the edge back to the parent is the same edge you just walked down. Counting it as a back edge makes every low[v] ≤ tin[u] and hides every bridge. If two parallel cables join u and its parent, the second one is a real backup — so then skip the parent edge id, not the parent vertex.
Strongly connected component vs connected component — what's the difference?
A connected component is undirected: "can I get there at all?". An SCC is directed, and you need a path both ways. With only a → b, a and b sit in two different SCCs, {a} and {b}, because you can't get back from b. Every vertex is in exactly one SCC; a vertex on no cycle is an SCC of size 1.
Kosaraju or Tarjan for SCCs?
Both are O(V + E). Kosaraju is easier to get right: two plain DFS passes, and pass 1 is the topological-sort finish order you already know. Tarjan is one pass and needs no reversed graph, but juggles low, a stack and onStack. Explain Kosaraju unless you're asked for one pass.
Evaluate Division: why does any path give the same answer?
Because the products cancel. a → x → b multiplies (a / x) × (x / b) = a / b — the x disappears. Every path from a to b cancels down to a / b the same way, as long as the equations don't contradict each other (LeetCode promises they don't). So the first path your DFS finds is enough; there is no "best" path to search for.
Evaluate Division: DFS or union-find?
At LeetCode's limits (20 equations, 20 queries) a DFS per query is the simplest correct answer. If there were 10⁵ queries on fixed equations, weighted union-find answers each in near-constant time after building — mention it as the follow-up.
Will recursive Tarjan pass LC 1192 in Java?
It depends on the JVM's stack size, which you don't control on a judge, and a 10⁵-server chain needs 10⁵ nested calls. Write the recursive version if you like, but name the risk. The fix is the explicit-stack version (shown below) or running the DFS in a Thread created with a bigger stack size.
10 · practice
Interview problems
Every problem has brute → better → optimal solutions in Java, with complexity and a dry run.
- #332
- #1192
- #399
- #815Bus RoutesHardUberAmazonGoogle
Also practise this here
Each problem has one home page in the course; these use this chapter's idea too. The note says what to look for.
- #1489Find Critical and Pseudo-Critical Edges in Minimum Spanning TreeHardin Union-Find & MST
"Critical" again, but for minimum spanning trees: an edge every MST needs. Try removing it and compare the MST weight.
- #207Course ScheduleMediumin Topo Sort
Course Schedule only asks whether a cycle exists. SCCs go further and tell you which courses are stuck together.
- #127Word LadderHardin Graphs
Another BFS where choosing the vertices (words) and generating neighbours is the real work — like Bus Routes.
- #1514Path with Maximum ProbabilityMediumin Shortest Paths
Multiply along a path again, like Evaluate Division — but now you want the BEST product, so it's Dijkstra with a max-heap.
- #787Cheapest Flights Within K StopsMediumin Shortest Paths
Flights as a weighted graph with a stop limit. Compare with Reconstruct Itinerary, which must use every ticket.
- #990Satisfiability of Equality EquationsMediumin Union-Find & MST
Union-find over relations. Add a weight per element and you get Evaluate Division's union-find approach.
11 · revise
The whole chapter on one screen
10 cards, about a minute. Read them the night before an interview, or right after the chapter to lock it in.
- what it is
Euler path = use every EDGE once. Bridge = an edge whose removal splits the graph. SCC = a largest group of vertices that all reach each other (directed).
- use it when
"Use every ticket once" → Hierholzer. "Critical connection" → low-link DFS. "Reach each other" → SCC. Ratios → weighted graph. "Fewest buses" → BFS over buses.
- template
Hierholzer:
visit(u): while u has unused edges,visit(next); thenroute.add(u). Reverse the route at the end. - template
Bridges:
tin[u] = low[u] = timer++; back edge →min(low[u], tin[v]); after child →min(low[u], low[v]); bridge ifflow[v] > tin[u]. - cost
Hierholzer O(E) (O(E log E) with min-heaps). Bridges, cut vertices, SCCs: O(V + E). Bus Routes over buses: O(Σ route lengths).
- trap
Greedy "smallest next airport" can hit a dead end. Add an airport only when it's STUCK, then reverse — that splices the loops in.
- trap
Bridges: skip the parent edge, use
>for bridges but≥for cut vertices, and the root is a cut vertex only with 2+ DFS children. - trap
Recursive DFS on a 10⁵-long chain can overflow Java's stack. Name the risk and offer the explicit-stack version.
- pro tip
Ratios: a → b carries a/b and b → a carries b/a. Multiply along any path. A variable never seen → -1.0, even for x / x.
- pro tip
Kosaraju = finish order, then DFS on the REVERSED graph, latest finisher first. Each DFS collects exactly one SCC.
12 · recall
Flip, remember, repeat
When does a directed graph have an Euler path?
Question 1: When does a directed graph have an Euler path?
13 · check yourself
Quick quiz
Directed edges 0→1, 1→2, 2→0, 0→3. Where must an Euler path start?
Tickets JFK→KUL, JFK→NRT, NRT→JFK. What does Reconstruct Itinerary return?
Tree edge u → v in a low-link DFS: tin[u] = 3 and low[v] = 3. Is u–v a bridge?
When is the DFS root an articulation point?
Directed edges 0→1, 1→2, 2→0, 2→3, 3→4. How many strongly connected components?
In Kosaraju's second pass, which graph is searched, and in which order?
Evaluate Division with a / b = 2. What is x / x if x appears in no equation?
In the optimal Bus Routes BFS, what does one BFS level stand for?
Finding all bridges with low-links costs…
14 · learn more
A second teacher, if you want one
- ▶ videoG-55. Bridges in Graph - Using Tarjan's Algorithm of time in and low time ↗Striver (take U forward) · EnglishBridges with discovery time and low time. The same idea powers articulation points (G-56).
- ▶ videoG-54. Strongly Connected Components - Kosaraju's Algorithm ↗Striver (take U forward) · EnglishStrongly connected components in three steps: order nodes by finish time, reverse the edges, then DFS again.
- ▶ videoTarjan's Strongly Connected Component (SCC) Algorithm (UPDATED) | Graph Theory ↗William Fiset · EnglishTarjan's one-pass SCC algorithm with clear low-link animations. Watch it after Kosaraju for the deeper version.