Cairn
algorithms · network flow · O(V) max-flow computations, deterministic

back to Network Flow

Gomory-Hu Tree

Edmonds-Karp, Dinic's Algorithm, and Push-Relabel all answer one max-flow question at a time: fix a source and a sink, get one number. Choosing a Network Flow Algorithm already notes a cheap way to get the graph's single weakest point instead — fix any one node, run n − 1 max-flow computations against every other node as sink, and the smallest of those is the true global minimum cut, no repeats needed. The Gomory-Hu tree, described by Ralph Gomory and Te Chiang Hu in 1961 (this page builds it via Dan Gusfield's considerably simpler 1990 construction), answers a broader question with that exact same budget of n − 1 max-flow computations: not just the single weakest cut, but the max-flow value between every one of a graph's n(n−1)/2 pairs of nodes, packed into a tree with only n − 1 edges.

Gusfield's construction builds that tree incrementally. Pick any node as an arbitrary root (this page always picks the first); every other node starts out with a tentative parent pointing at the root. Then, one node i at a time: run one real max-flow between i and whatever it currently points at, record that flow as i's tentative edge weight, and do two pieces of bookkeeping — any later node still pointing at the same parent that landed on i's side of this computation's min cut reparents to i instead (its own eventual answer is now more accurately routed through i); and if i's parent's own parent also landed on i's side, the two edges swap places (see Try it's own trace for exactly when and why — it happens on this page's own default graph). Every one of these max-flow computations runs on the same, unmodified original graph — nothing shrinks or contracts between steps, unlike Stoer-Wagner Algorithm's phase-by-phase merging for the single global-minimum-cut question.

Try it

Six nodes, six weighted undirected edges: A-D (4), B-C (8), B-D (6), C-E (7), C-F (6), D-F (3). Press Step or Run to process nodes B through F in order: each step's max-flow computation highlights the source node (bold border) and its target parent (thin border), and shades every node on the source's side of that computation's cut. The table below the graph tracks each node's current tentative parent and weight live, including the one reparent-and-swap event this graph triggers. Once the tree is complete, use the query tool to pick any two nodes — the smallest weight on the tree path between them is checked live against a real max-flow computation on the original graph below it.

step — / 5 · tree edges fixed: 0 / 5
Press Step or Run.

Why the tree's own path answers every pair

The read-off rule: the max-flow value between any two nodes u and v equals the smallest edge weight on the unique tree path between them — never the sum, never the largest, always the minimum. That single number is guaranteed correct for every pair (verified below), which is a genuinely different guarantee from what a naive fix-one-source approach gives (see Pitfalls). Checked exhaustively over every one of the 1,024 possible unweighted graphs on 5 labeled vertices (10,240 total pairs) and via 5,000 randomized weighted trials on graphs of 4-9 vertices (95,883 total pairs) against independent brute-force max-flow on each pair — 0 mismatches, both times.

Two honest caveats worth knowing rather than assuming. First, a tree edge does not need to be a real edge in the original graph at all — it's a max-flow value, not a capacity. On this page's own demo graph, all five tree edges happen to coincide with real original edges (with different weights — the original B-D edge is capacity 6, but the tree's B-D edge is weight 9), purely because the graph is small and dense; measured across 2,000 randomized larger graphs, 24.3% of all tree edges connect a pair with no direct edge between them whatsoever. Second, this page builds the tree via Gusfield's simplification specifically because it's dramatically simpler to implement than Gomory and Hu's original 1961 construction (which contracts one side of the graph after every step) — verified here to reproduce the correct max-flow value for every pair, which is everything a query needs. The original, more involved construction additionally guarantees that the edge removed to separate any two nodes is an actual minimum cut for that pair in the original graph, a stronger property this page's simpler version doesn't claim and isn't needed for the value-lookup this page's query tool performs.

Reference implementation

Matches the demo above one for one (with the demo's own bookkeeping variants stripped out — this is the correct, unmodified construction):

function gomoryHuTree(n, cap) {
  // cap: n x n symmetric capacity matrix, node 0 is the arbitrary root
  const parent = new Array(n).fill(0);
  const weight = new Array(n).fill(0);

  for (let i = 1; i < n; i++) {
    const { flow, sSide } = maxFlowWithMinCutSide(n, cap, i, parent[i]);
    const oldParent = parent[i];
    weight[i] = flow;

    for (let j = i + 1; j < n; j++) {
      if (parent[j] === oldParent && sSide[j]) parent[j] = i;
    }

    const grandparent = parent[oldParent];
    if (sSide[grandparent]) {
      parent[i] = grandparent;
      parent[oldParent] = i;
      weight[i] = weight[oldParent];
      weight[oldParent] = flow;
    }
  }
  return { parent, weight }; // edge (i, parent[i]) has weight weight[i], for i = 1..n-1
}

function queryMaxFlow(n, parent, weight, u, v) {
  // BFS the tree, tracking the minimum edge weight seen along the path
  const adj = Array.from({ length: n }, () => []);
  for (let i = 1; i < n; i++) {
    adj[i].push([parent[i], weight[i]]);
    adj[parent[i]].push([i, weight[i]]);
  }
  const seen = new Array(n).fill(false);
  seen[u] = true;
  const queue = [[u, Infinity]];
  while (queue.length) {
    const [node, minSoFar] = queue.shift();
    if (node === v) return minSoFar;
    for (const [next, w] of adj[node]) {
      if (!seen[next]) { seen[next] = true; queue.push([next, Math.min(minSoFar, w)]); }
    }
  }
}

Pitfalls

The reparent-and-swap step must read the old parent before overwriting it. Computing parent[i] = grandparent and then using parent[i] — already overwritten — to index the second half of the swap (parent[parent[i]] = i) silently writes into the wrong slot instead of setting the old parent's own entry. It still runs, still returns a tree of the right shape, just with one edge's weight lost, permanently replaced by its unused initial value of 0. On this page's own default graph, that one corrupted node is D: the buggy tree reports max-flow of 0 for every one of the 5 pairs involving D (A-D, B-D, C-D, D-E, D-F), each falsely claiming D is fully disconnected, when the true values are 4, 9, 9, 7, and 9 — select "reparent using the parent pointer after it's already been overwritten" above and query any pair with D to see it live. Measured more broadly: across 5,000 randomized weighted graphs (4-9 vertices, 95,509 total pairs), this bug disagreed with brute-force max-flow on 25.6% of all pairs — not rare, and not limited to unusual graphs.

"Fix one source, run n − 1 max-flows" is a real, working algorithm — just not this one. The Network Flow guide's own naive method for the global minimum cut (fix any node, run n − 1 max-flow computations against every other node as sink, keep the smallest) looks identical to this page's own budget, and it's tempting to assume it's silently already building a Gomory-Hu tree, giving every pairwise answer along the way. It isn't: fixing the source instead of using each node's current parent still finds the correct global minimum every time (confirmed over 3,000 randomized trials, 3,000/3,000), but reports the wrong value for almost every pair that doesn't include the fixed root. On this page's own demo graph, using this fixed-source shortcut instead of Gusfield's reparenting gets 10 of the 15 possible pairs wrong — every pair not involving A — each one under-reported to exactly 4 (the graph's true global minimum, and also A's own bottleneck edge), when the true values range from 7 to 11. Select "always flow against the root" above to see it live. Measured more broadly: 22.6% of all pairs wrong across 3,000 randomized graphs. The reparenting step — not just the raw count of max-flow computations — is what turns a global-minimum-cut trick into a tree that answers every pair.

Complexity

Time: n − 1 max-flow computations, each run on the full, unshrunk original graph — O(V) calls total, each costing whatever the underlying max-flow algorithm costs on the original V and E. This page's demo and reference implementation use the same Edmonds-Karp subroutine already on this site (O(VE²) per call), for a combined O(V²E²) as implemented here — swapping in Dinic's Algorithm underneath instead would tighten that to O(V²E), since nothing about Gusfield's construction depends on which max-flow subroutine answers each call. Space: O(V²) for the dense capacity matrix each max-flow call reads, plus O(V) for the tree's own parent/weight arrays.

Compare that O(V) max-flow calls against computing every pair's max flow independently — the naive way to answer "what's the max flow between every pair" without this page's tree, at O(V²) separate calls. See Choosing a Network Flow Algorithm for how this entry compares against all ten of the site's other Network Flow pages side by side.