Chapter 41: Advanced Graph Structure and Specialized Graph Problems
41.07Intermediate / Advanced4 topics17 min2 interactive animations

Directed specialized

Directed Euler circuits solve the Chinese postman problem, produce de Bruijn sequences that contain every word of a given length exactly once, and generate universal cycles for codes and locks.

Learning goals

  • Solve the Chinese postman problem for undirected graphs
  • Build de Bruijn graphs and understand their Euler circuits
  • Verify universal cycles
  • Construct Euler trails recursively

Prerequisites

Euler trails (Lesson 41.01)All-pairs shortest paths (Chapter 35)Bitmask DP (Lesson 41.02)

A snow plough must pass every street at least once and return to the depot; a lock with a 4-digit code could be cracked by the shortest string that contains every code. Both are Euler-circuit problems in a graph built for the purpose.

41.07.1

Chinese postman overview

The **Chinese postman problem** (route inspection) asks for the shortest closed walk that uses every edge of a connected weighted graph at least once. If every degree is even, an Euler circuit exists and its length is simply the sum of the edge weights. Otherwise some edges have to be walked twice.

Repeating a path between two odd vertices makes both of them even. So the task is to **pair up the odd vertices** (there is always an even number of them) so that the total length of the connecting shortest paths is minimum. The shortest distances come from Floyd-Warshall; the pairing is a minimum-weight perfect matching on the odd vertices, which for a few odd vertices is a simple bitmask DP (for many, the blossom algorithm of Chapter 40). The answer is the total weight plus the cost of the best pairing.

Take the 4-vertex graph with edges 0-1:2, 1-2:3, 2-3:4, 3-0:5 and 0-2:1. Vertices 0 and 2 have degree 3, the others 2. The cheapest way between them is the edge 0-2 of weight 1, so the shortest postman tour has length 2 + 3 + 4 + 5 + 1 + 1 = 16. For directed graphs the same idea works with in/out balance and a minimum-cost flow instead of a matching.

Java
1// edges {a, b, weight} of a connected undirected graph; returns the length of the shortest closed walk using every edge at least once
2static int chinesePostman(int n, int[][] edges) {
3 int inf = Integer.MAX_VALUE / 4, total = 0;
4 int[][] dist = new int[n][n];
5 for (int[] row : dist) Arrays.fill(row, inf);
6 for (int i = 0; i < n; i++) dist[i][i] = 0;
7 int[] degree = new int[n];
8 for (int[] e : edges) {
9 total += e[2];
10 degree[e[0]]++;
11 degree[e[1]]++;
12 dist[e[0]][e[1]] = Math.min(dist[e[0]][e[1]], e[2]);
13 dist[e[1]][e[0]] = Math.min(dist[e[1]][e[0]], e[2]);
14 }
15 for (int k = 0; k < n; k++) // Floyd-Warshall: the cheapest way between any two vertices
16 for (int i = 0; i < n; i++)
17 for (int j = 0; j < n; j++)
18 if (dist[i][k] + dist[k][j] < dist[i][j]) dist[i][j] = dist[i][k] + dist[k][j];
19 List<Integer> odd = new ArrayList<>();
20 for (int v = 0; v < n; v++) if (degree[v] % 2 == 1) odd.add(v);
21 int m = odd.size();
22 int[] best = new int[1 << m]; // best[mask]: cheapest way to pair up the odd vertices in mask
23 Arrays.fill(best, inf);
24 best[0] = 0;
25 for (int mask = 0; mask < (1 << m); mask++) {
26 if (best[mask] >= inf) continue;
27 int i = 0;
28 while (i < m && (mask >> i & 1) == 1) i++; // pair the lowest unpaired odd vertex with some other one
29 if (i == m) continue;
30 for (int j = i + 1; j < m; j++)
31 if ((mask >> j & 1) == 0)
32 best[mask | 1 << i | 1 << j] = Math.min(best[mask | 1 << i | 1 << j], best[mask] + dist[odd.get(i)][odd.get(j)]);
33 }
34 return total + best[(1 << m) - 1]; // every pairing duplicates a shortest path
35}

Worst

O(V³ + 2^m · m²) for m odd vertices

Space

O(V² + 2^m)

41.07.2

De Bruijn graphs

The **de Bruijn graph** B(k, n) has one vertex for every word of length n − 1 over an alphabet of k symbols, and one edge for every word of length n: the word a1 a2 … an is an edge from a1 … a(n−1) to a2 … an. There are k^(n−1) vertices and k^n edges, and every vertex has exactly k edges in and k edges out.

Because in-degree equals out-degree everywhere and the graph is connected, it has an **Euler circuit**. Walking it uses every edge, that is every word of length n, exactly once. Writing down the symbol of each edge along the circuit gives a cyclic sequence of length k^n in which every word of length n appears exactly once: a **de Bruijn sequence**.

Open the animation with the default k = 2, n = 3: the graph has 4 nodes (00, 01, 10, 11) and 8 edges, Hierholzer's algorithm follows them with a stack, and the symbols come out in reverse order, forming 01011100. Read cyclically, its 8 windows of length 3 are 010, 101, 011, 111, 110, 100, 000 and 001: all eight binary words. Other presets show k = 3, n = 2 (010211220) and k = 2, n = 4.

Java
1static String deBruijn(int k, int n) { // a cyclic string in which every length-n word over k symbols appears once
2 int nodes = 1;
3 for (int i = 1; i < n; i++) nodes *= k; // a node is a word of length n - 1
4 int[] next = new int[nodes]; // next unused outgoing symbol of each node
5 Deque<int[]> stack = new ArrayDeque<>();
6 stack.push(new int[]{0, -1}); // {node, symbol of the edge that led here}
7 StringBuilder out = new StringBuilder();
8 while (!stack.isEmpty()) { // Hierholzer on the de Bruijn graph: every node has in = out = k
9 int[] top = stack.peek();
10 if (next[top[0]] < k) {
11 int c = next[top[0]]++;
12 stack.push(new int[]{(top[0] * k + c) % nodes, c}); // follow an edge: drop the first symbol, append c
13 } else {
14 stack.pop();
15 if (top[1] >= 0) out.append(top[1]); // symbols come out in reverse order
16 }
17 }
18 return out.reverse().toString();
19}

Worst

O(k^n)

Space

O(k^(n−1))

De Bruijn sequences: an Euler circuit in the graph of overlapping words

Interactive · edit the input

Your input

Try

nodes are words of length 2; an edge labelled c leads from w to (w without its first symbol) + c · 4 vertices · 8 edges

00
01
10
11
Edge just followedOn the stackEdge already used

stack of nodes (top on the right)

00

symbols output so far (reverse order)

empty
Setup

Every word of length 3 is an edge from its first 2 symbols to its last 2 symbols. Each node has 2 edges in and 2 out, so an Euler circuit exists; the symbols along that circuit spell a cyclic sequence that contains every word of length 3 exactly once. Hierholzer's algorithm builds it.

Step 1 / 19
Java
1static String deBruijn(int k, int n) { // a cyclic string in which every length-n word over k symbols appears once
2 int nodes = 1;
3 for (int i = 1; i < n; i++) nodes *= k; // a node is a word of length n - 1
4 int[] next = new int[nodes]; // next unused outgoing symbol of each node
5 Deque<int[]> stack = new ArrayDeque<>();
6 stack.push(new int[]{0, -1}); // {node, symbol of the edge that led here}
7 StringBuilder out = new StringBuilder();
8 while (!stack.isEmpty()) { // Hierholzer on the de Bruijn graph: every node has in = out = k
9 int[] top = stack.peek();
10 if (next[top[0]] < k) {
11 int c = next[top[0]]++;
12 stack.push(new int[]{(top[0] * k + c) % nodes, c}); // follow an edge: drop the first symbol, append c
13 } else {
14 stack.pop();
15 if (top[1] >= 0) out.append(top[1]); // symbols come out in reverse order
16 }
17 }
18 return out.reverse().toString();
19}

Live state

0 edges followed0 symbols output

k

2

n

3

nodes

4

edges

8

No condition evaluated on this step
Execution trace (dry run table)Show 19 steps
StepLineStateWhat happens
1L1k=2 n=3 nodes=4 edges=8Every word of length 3 is an edge from its first 2 symbols to its last 2 symbols. Each node has 2 edges in and 2 out, so an Euler circuit exists; the symbols along that circuit spell a cyclic sequence that contains every word of length 3 exactly once. Hierholzer's algorithm builds it.
2L12from=00 symbol=0 to=00From 00 follow the edge with symbol 0 to 00 (the word 000 is now used).
3L12from=00 symbol=1 to=01From 00 follow the edge with symbol 1 to 01 (the word 001 is now used).
4L12from=01 symbol=0 to=10From 01 follow the edge with symbol 0 to 10 (the word 010 is now used).
5L12from=10 symbol=0 to=00From 10 follow the edge with symbol 0 to 00 (the word 100 is now used).
6L15stuck=00 appended=000 has no unused edge: output the symbol 0 that led here and back up.
7L12from=10 symbol=1 to=01From 10 follow the edge with symbol 1 to 01 (the word 101 is now used).
8L12from=01 symbol=1 to=11From 01 follow the edge with symbol 1 to 11 (the word 011 is now used).
9L12from=11 symbol=0 to=10From 11 follow the edge with symbol 0 to 10 (the word 110 is now used).
10L15stuck=10 appended=010 has no unused edge: output the symbol 0 that led here and back up.
11L12from=11 symbol=1 to=11From 11 follow the edge with symbol 1 to 11 (the word 111 is now used).
12L15stuck=11 appended=111 has no unused edge: output the symbol 1 that led here and back up.
13L15stuck=11 appended=111 has no unused edge: output the symbol 1 that led here and back up.
14L15stuck=01 appended=101 has no unused edge: output the symbol 1 that led here and back up.
15L15stuck=10 appended=010 has no unused edge: output the symbol 0 that led here and back up.
16L15stuck=01 appended=101 has no unused edge: output the symbol 1 that led here and back up.
17L15stuck=00 appended=000 has no unused edge: output the symbol 0 that led here and back up.
18L15stuck=00 appended=noneThe start node has no unused edge: the circuit is complete.
19L18return=01011100 length=8 distinct_words=8Sequence 01011100 (length 8 = 2^3). Reading it cyclically, its 8 windows of length 3 are 8 different words, so every word over 2 symbols appears exactly once.
41.07.3

Universal cycles

A **universal cycle** (a de Bruijn sequence in the cyclic sense) for words of length n over k symbols is a cyclic string of length k^n in which each word occurs exactly once as a window. It is the shortest string that can contain every word of length n when the string is allowed to wrap around, and k^n + n − 1 characters if it is read linearly (repeat the first n − 1 symbols at the end).

Applications include brute-forcing keypad locks (the lock checks the last n digits typed; a de Bruijn sequence for 4 digits has 10,000 symbols instead of 40,000 for trying each code separately), position encoding (a device that reads n consecutive bits learns its absolute position on a circular track), and testing hardware by feeding it every input pattern.

The checker below is a direct definition: confirm the length is k^n, then slide a window over the string extended by its first n − 1 symbols and make sure no window repeats. Since the sequence has exactly k^n windows, “no repeats” already means every word appears. Up to rotation there are 2^(2^(n−1) − n) binary de Bruijn sequences: 2 for n = 3 and 16 for n = 4.

Java
1// true when every word of length n over the symbols 0 .. k-1 appears exactly once when s is read cyclically
2static boolean isUniversalCycle(String s, int k, int n) {
3 int words = 1;
4 for (int i = 0; i < n; i++) words *= k;
5 if (s.length() != words) return false; // k^n windows can hold at most k^n distinct words
6 for (char c : s.toCharArray()) if (c < '0' || c >= '0' + k) return false;
7 String doubled = s + s.substring(0, n - 1); // append the start so that windows can wrap round
8 Set<String> seen = new HashSet<>();
9 for (int i = 0; i < s.length(); i++) if (!seen.add(doubled.substring(i, i + n))) return false;
10 return true;
11}
41.07.4

Euler trail construction

Hierholzer's algorithm for directed graphs has an elegant recursive form. From a vertex v, follow every unused outgoing arc recursively; when v has none left, **prepend** v to the circuit. The recursion explores a closed walk, and when it returns it has produced the vertices in reverse finishing order, which is exactly an Euler circuit.

The code assumes in-degree equals out-degree everywhere and that the arcs are connected; check this first with the balance and connectivity conditions of Lesson 41.01. For a trail instead of a circuit, start the recursion at the vertex with out − in = +1; the finishing order then gives a trail from that vertex to the one with −1. The next[] pointer is shared across all calls so that each arc is traversed once: O(V + E) total.

The recursion depth can reach the number of arcs, so a very large graph needs the iterative stack version shown in the animation (use the directed presets: the circuit 0→1→2→3→1→3→0 and the trail 2→0→1→2→3).

Java
1// recursive Hierholzer on a directed graph whose in-degree and out-degree agree everywhere and whose arcs are connected
2static List<Integer> eulerCircuitDirected(int n, List<Integer>[] out, int start) {
3 LinkedList<Integer> circuit = new LinkedList<>();
4 visit(start, out, new int[n], circuit);
5 return circuit;
6}
7static void visit(int v, List<Integer>[] out, int[] next, LinkedList<Integer> circuit) {
8 while (next[v] < out[v].size()) visit(out[v].get(next[v]++), out, next, circuit); // follow every unused arc, depth first
9 circuit.addFirst(v); // v is final once all its arcs are used
10}

Worst

O(V + E)

Space

O(V + E) (recursion depth up to E)

Euler trails and circuits: degree conditions and Hierholzer's algorithm

Interactive · edit the input

Your input

Try

undirected graph · 5 vertices · 6 edges

0

deg 2

1

deg 2

2

deg 4

3

deg 2

4

deg 2

Top of the stack / edge just takenOn the stackEdge used or vertex in the trail

degree of each vertex

20
21
42
23
24

stack (top on the right)

empty

trail built so far (front first)

empty
Setup

An Euler trail uses every edge exactly once. The degree of each vertex is shown: a circuit needs every degree even; a trail needs exactly two odd vertices (the ends).

Step 1 / 17
Java
1// edges {a, b}; returns the vertices of a trail that uses every edge exactly once, or null when none exists
2@SuppressWarnings("unchecked")
3static List<Integer> eulerTrail(int n, int[][] edges, boolean directed) {
4 List<int[]>[] adj = new List[n]; // arc {neighbour, edge id}
5 int[] balance = new int[n]; // directed: out - in; undirected: degree
6 for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();
7 for (int id = 0; id < edges.length; id++) {
8 int a = edges[id][0], b = edges[id][1];
9 adj[a].add(new int[]{b, id});
10 if (directed) { balance[a]++; balance[b]--; }
11 else { adj[b].add(new int[]{a, id}); balance[a]++; balance[b]++; }
12 }
13 int start = -1, plus = 0, minus = 0, odd = 0;
14 for (int v = 0; v < n; v++) { // 1. degree / balance conditions
15 if (directed) {
16 if (balance[v] == 1) { plus++; start = v; } // one vertex may have one more edge out than in
17 else if (balance[v] == -1) minus++; // and one may have one more in than out
18 else if (balance[v] != 0) return null;
19 } else if (balance[v] % 2 == 1) {
20 odd++;
21 if (start == -1) start = v; // a trail must start at an odd vertex
22 }
23 }
24 if (directed ? plus != minus || plus > 1 : odd != 0 && odd != 2) return null;
25 if (start == -1) start = edges[0][0]; // all balanced: a circuit may start anywhere
26 boolean[] used = new boolean[edges.length];
27 int[] next = new int[n]; // next unexplored arc of each vertex
28 Deque<Integer> stack = new ArrayDeque<>(List.of(start));
29 LinkedList<Integer> trail = new LinkedList<>();
30 while (!stack.isEmpty()) { // 2. Hierholzer
31 int v = stack.peek();
32 while (next[v] < adj[v].size() && used[adj[v].get(next[v])[1]]) next[v]++; // skip edges used from the other end
33 if (next[v] == adj[v].size()) { trail.addFirst(v); stack.pop(); } // stuck: v is final in the trail
34 else {
35 int[] arc = adj[v].get(next[v]++);
36 used[arc[1]] = true;
37 stack.push(arc[0]);
38 }
39 }
40 return trail.size() == edges.length + 1 ? trail : null; // too short: some edges are in another component
41}

Live state

0 edges used0 dead ends

n

5

edges

6

kind

undirected

No condition evaluated on this step
Execution trace (dry run table)Show 17 steps
StepLineStateWhat happens
1L3n=5 edges=6 kind=undirectedAn Euler trail uses every edge exactly once. The degree of each vertex is shown: a circuit needs every degree even; a trail needs exactly two odd vertices (the ends).
2L24start=0 kind=circuitEvery vertex is balanced, so a circuit can start anywhere; start at 0. Connectivity is still unchecked; Hierholzer's algorithm will reveal it.
3L28start=0Hierholzer: walk along unused edges until stuck, pushing vertices on a stack. A stuck vertex is final, so it moves to the front of the trail; then go back and splice in the detours.
4L37from=0 to=1 edge=0Take the unused edge 0-1 and move to 1.
5L37from=1 to=2 edge=1Take the unused edge 1-2 and move to 2.
6L37from=2 to=0 edge=2Take the unused edge 2-0 and move to 0.
7L33stuck=0 trail=0Vertex 0 has no unused edge left: it is final, so it goes to the front of the trail. Back up to 2.
8L37from=2 to=3 edge=3Take the unused edge 2-3 and move to 3.
9L37from=3 to=4 edge=4Take the unused edge 3-4 and move to 4.
10L37from=4 to=2 edge=5Take the unused edge 4-2 and move to 2.
11L33stuck=2 trail=2→0Vertex 2 has no unused edge left: it is final, so it goes to the front of the trail. Back up to 4.
12L33stuck=4 trail=4→2→0Vertex 4 has no unused edge left: it is final, so it goes to the front of the trail. Back up to 3.
13L33stuck=3 trail=3→4→2→0Vertex 3 has no unused edge left: it is final, so it goes to the front of the trail. Back up to 2.
14L33stuck=2 trail=2→3→4→2→0Vertex 2 has no unused edge left: it is final, so it goes to the front of the trail. Back up to 1.
15L33stuck=1 trail=1→2→3→4→2→0Vertex 1 has no unused edge left: it is final, so it goes to the front of the trail. Back up to 0.
16L33stuck=0 trail=0→1→2→3→4→2→0Vertex 0 has no unused edge left: it is final, so it goes to the front of the trail.
17L40return=0→1→2→3→4→2→0 edges=6Euler circuit: 0→1→2→3→4→2→0. All 6 edges are used exactly once.

Practice ladder

Now try it yourself

Easy

  • Find Center of Star Graph

Medium

  • Reconstruct Itinerary

Advanced

  • Cracking the Safe
  • Valid Arrangement of Pairs

Key takeaways

  • Chinese postman: total weight + cheapest pairing of the odd vertices by shortest paths.
  • A de Bruijn graph has an Euler circuit; its edge symbols spell a sequence containing every length-n word exactly once.
  • Recursive Hierholzer prepends a vertex when its arcs are exhausted.