30

Advanced Graphs: Euler Paths, Bridges & SCCs

Use every edge once, find the single points of failure, and group what can reach itself.

advanced~90 min5 simulations4 problems

▶ 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 (a used[] pointer for int vertices); a recursive post-order visit or an ArrayDeque stack; add a vertex when stuck, Collections.reverse at the end.

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 smallest tin that u's subtree can reach with one back edge. A tree edge u–v is a bridge iff low[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 = k gives 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.

sim

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.

loading simulation…
sim

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

loading simulation…
sim

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.

loading simulation…
sim

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.

loading simulation…
sim

Choosing the vertices — Bus Routes

Boxes are buses (the BFS vertices), spheres are stops. One BFS level = one more bus boarded.

loading simulation…

05 · cost

Time & space, explained

Euler paths, bridges and SCCs (V vertices, E edges)
TimeExtra spacewhy
Euler degree checkO(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 itineraryexponential (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 connectivityO(E · (V + E))O(V + E)E separate BFS runs, each O(V + E).
Kosaraju SCCO(V + E)O(V + E)Two DFS passes. The extra O(E) is the reversed copy of the graph that pass 2 walks.
Tarjan SCCO(V + E)O(V) beyond the graphOne DFS; each vertex is pushed on and popped off the stack once. No reversed copy — just tin, low, onStack and the stack.
Weighted relations and derived graphs
TimeExtra spacewhy
Evaluate Division: DFS per queryO(Q · (V + E))O(V + E)Each of the Q walks may visit every variable and edge once.
Evaluate Division: all-pairs closureO(V³ + Q)O(V²)Every middle variable is tried for every pair (V³); then each query is a table lookup.
Evaluate Division: weighted union-findO((E + Q) · log V) amortizedO(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 stopsO(Σ Lᵢ²)O(Σ Lᵢ²)A route with L stops joins every pair of them: L · (L − 1) directed edges per route.
Bus Routes: BFS over busesO(Σ 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

javain/out-degree checkjava.util.*
/** 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}
  • 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 number used[u] is the next unused one. No remove calls, so every step is O(1).
  • Push while there is an unused edge; pop into path when stuck. The popped order is the trip backwards — Collections.reverse once 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[]

javabridges: low[v] > tin[u]java.util.*
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            }        }    }}
  • Fill tin with -1 so it doubles as the visited check. low[u] starts equal to tin[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.java does; LC 1192's network is connected, so one DFS from 0 is enough there.

Deep graphs in Java: recursion vs an explicit stack

javarecursive low-link DFSjava.util.*
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            }        }    }}
  • 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. peek the top: if it has a neighbour left, handle it (maybe push a child); otherwise pop and update the parent.
  • "Returning" from a child is the moment you pop it: that's where low[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

javaTarjan: one DFS, low-links + stackjava.util.*
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++;        }    }}
  • 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 ArrayDeque when 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 lower low[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

javaweighted union-findjava.util.*
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)    }}
  • graph.computeIfAbsent(a, k -> new HashMap<>()).put(b, value) — and the reverse edge with 1.0 / value. Forgetting the reverse is the #1 bug.
  • HashMap visits 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). find multiplies 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.
Java API you will actually use
calldoescost
new PriorityQueue<String>()Min-heap of destinations: poll() gives the lexicographically smallestO(log n) offer/poll
map.computeIfAbsent(k, x -> new PriorityQueue<>()).offer(v)Adjacency with sorted neighbours for string verticesO(1) average + O(log n)
Collections.reverse(list)Turn the post-order route into the real tripO(n)
deque.push(x) / deque.peek() / deque.pop()Explicit DFS stack instead of recursionO(1)
Arrays.fill(tin, -1)Mark every vertex unvisited before a low-link DFSO(n)
Math.min(low[u], low[v])Pull a child's reach up into its parentO(1)
List.of(u, v)An immutable pair for the answer listO(1)
map.getOrDefault(stop, List.of())Buses at a stop, or an empty list for a stop no bus visitsO(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 ArrayList for every ticket is O(E) each. Use a PriorityQueue (or a sorted ArrayDeque) 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 / x when 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/low next 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.

  1. #332
    Reconstruct Itinerarymust-do
    HardNetflixUberAmazonGoogleMeta
  2. #1192
    Critical Connections in a Networkmust-do
    HardAmazonGoogleMeta
  3. #399
    Evaluate Divisionmust-do
    MediumUberBloombergAmazonGoogleMeta
  4. #815
    Bus Routes
    HardUberAmazonGoogle

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.

  1. #1489
    Find Critical and Pseudo-Critical Edges in Minimum Spanning Tree
    Hardin Union-Find & MST

    "Critical" again, but for minimum spanning trees: an edge every MST needs. Try removing it and compare the MST weight.

  2. #207
    Course Schedule
    Mediumin Topo Sort

    Course Schedule only asks whether a cycle exists. SCCs go further and tell you which courses are stuck together.

  3. #127
    Word Ladder
    Hardin Graphs

    Another BFS where choosing the vertices (words) and generating neighbours is the real work — like Bus Routes.

  4. #1514
    Path with Maximum Probability
    Mediumin 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.

  5. #787
    Cheapest Flights Within K Stops
    Mediumin Shortest Paths

    Flights as a weighted graph with a stop limit. Compare with Reconstruct Itinerary, which must use every ticket.

  6. #990
    Satisfiability of Equality Equations
    Mediumin 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.

  1. 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).

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

  3. template

    Hierholzer: visit(u): while u has unused edges, visit(next); then route.add(u). Reverse the route at the end.

  4. template

    Bridges: tin[u] = low[u] = timer++; back edge → min(low[u], tin[v]); after child → min(low[u], low[v]); bridge iff low[v] > tin[u].

  5. 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).

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

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

  8. trap

    Recursive DFS on a 10⁵-long chain can overflow Java's stack. Name the risk and offer the explicit-stack version.

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

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

card 1 / 15
recall

When does a directed graph have an Euler path?

tap or Space to flip ↻

Question 1: When does a directed graph have an Euler path?

13 · check yourself

Quick quiz

  1. Directed edges 0→1, 1→2, 2→0, 0→3. Where must an Euler path start?

  2. Tickets JFK→KUL, JFK→NRT, NRT→JFK. What does Reconstruct Itinerary return?

  3. Tree edge u → v in a low-link DFS: tin[u] = 3 and low[v] = 3. Is u–v a bridge?

  4. When is the DFS root an articulation point?

  5. Directed edges 0→1, 1→2, 2→0, 2→3, 3→4. How many strongly connected components?

  6. In Kosaraju's second pass, which graph is searched, and in which order?

  7. Evaluate Division with a / b = 2. What is x / x if x appears in no equation?

  8. In the optimal Bus Routes BFS, what does one BFS level stand for?

  9. Finding all bridges with low-links costs…