Max Flow
in reviewThis lesson is complete and being validated before release - details may still change.
in reviewThis lesson is complete and being validated before release - details may still change.
max-flow.ts
import type { Tracer } from "../trace";import type { FlowEdge, FlowNetwork } from "./flow-network";/** One round's path from source to sink through the residual graph, and the room it had. */export interface AugmentingPath { readonly nodes: readonly number[]; readonly bottleneck: number;}/** The nodes the last search reached, the original edges leaving that set, and their total capacity. */export interface MinCut { readonly sourceSide: readonly number[]; readonly edges: readonly FlowEdge[]; readonly capacity: number;}export interface MaxFlowResult { readonly flow: number; readonly paths: readonly AugmentingPath[]; readonly cut: MinCut; /** How many searches ran: one per path, plus the last one that found none. */ readonly rounds: number;}/** * Edmonds-Karp: the Ford-Fulkerson method with breadth-first search * choosing each augmenting path, so every round takes a path with the * fewest arcs. Each round searches the residual graph from the source, * taking a node's arcs in the network's adjacency order (edge order, * forward arcs at the tail, reverse arcs at the head) through a FIFO * queue, and stops the moment the sink is reached. The path's * bottleneck is its smallest residual; pushing it along every arc * raises the flow on forward arcs and cancels flow on reverse ones. * When a search ends without reaching the sink, the nodes it reached * are the source side of a minimum cut: every original edge leaving * that set is saturated and every edge entering it is empty, so their * capacity equals the flow (the max-flow min-cut theorem). * * The traces depend on that arc order, so fixtures pin it. `run` resets * the network first and leaves the maximum flow on it. * * Time: O(V E^2), at most O(V E) rounds of an O(E) search. Space: O(V). */export class MaxFlow { constructor( readonly network: FlowNetwork, private readonly tracer?: Tracer, ) {} run(source: number, sink: number): MaxFlowResult { const n = this.network.nodeCount; for (const end of [source, sink]) { if (!Number.isInteger(end) || end < 0 || end >= n) throw new RangeError(`${end} is not a node`); } if (source === sink) throw new RangeError("the source and the sink must differ"); this.network.reset(); const paths: AugmentingPath[] = []; let flow = 0; let rounds = 0; let reached: boolean[] = []; for (;;) { rounds++; // ▸ a new round: breadth first from the source over arcs with room this.tracer?.emit("search", { round: rounds, sink, source }); const parentArc = new Array<number>(n).fill(-1); const arcThatReached = new Array<number>(n).fill(-1); reached = Array.from({ length: n }, () => false); reached[source] = true; const queue = [source]; let found = false; let sinkReached = false; while (queue.length > 0 && !found) { while (queue.length > 0 && !sinkReached) { const node = queue.shift()!; for (const { arc, to, residual } of this.network.residual(node)) { if (reached[to]) continue; reached[to] = true; parentArc[to] = arc; arcThatReached[to] = arc; queue.push(to); // ▸ a node seen for the first time this round joins the queue this.tracer?.emit("reach", { from: node, node: to, queue: [...queue], residual }); if (to === sink) { found = true; sinkReached = true; break; } } } if (!found) break; if (!sinkReached) break; const path = this.augment(source, sink, parentArc, rounds); const path = this.augment(source, sink, arcThatReached, rounds); paths.push(path); flow += path.bottleneck; } const sourceSide: number[] = []; reached.forEach((inside, node) => { if (inside) sourceSide.push(node); }); const edges = this.network.edges.filter((edge) => reached[edge.from] && !reached[edge.to]); const capacity = edges.reduce((sum, edge) => sum + edge.capacity, 0); // ▸ no path remains: what the search reached is the source side of the cut this.tracer?.emit("cut", { capacity, edges: edges.map(({ from, to, capacity: c }) => ({ from, to, capacity: c })), sourceSide: [...sourceSide] }); // ▸ the flow equals the cut's capacity, so it is the maximum this.tracer?.emit("verdict", { flow, rounds }); return { flow, paths, cut: { sourceSide, edges, capacity }, rounds }; } /** Walks the parent arcs back from the sink, then pushes the bottleneck along the path, source first. */ private augment(source: number, sink: number, parentArc: readonly number[], round: number): AugmentingPath { private augment(source: number, sink: number, arcThatReached: readonly number[], round: number): AugmentingPath { const arcs: number[] = []; const pathArcs: number[] = []; const nodes = [sink]; for (let node = sink; node !== source; ) { const arc = parentArc[node]!; const arc = arcThatReached[node]!; arcs.push(arc); pathArcs.push(arc); node = this.network.arcEnds(arc).from; nodes.push(node); } arcs.reverse(); pathArcs.reverse(); nodes.reverse(); let bottleneck = Infinity; for (const arc of arcs) { for (const arc of pathArcs) { const { from } = this.network.arcEnds(arc); const room = this.network.residual(from).find((entry) => entry.arc === arc)!.residual; bottleneck = Math.min(bottleneck, room); } // ▸ the sink was reached: walk the parents back, the least room is the bottleneck this.tracer?.emit("path", { bottleneck, nodes: [...nodes], round }); for (const arc of arcs) { for (const arc of pathArcs) { const { from, to, reverse, edge } = this.network.arcEnds(arc); this.network.push(arc, bottleneck); const owner = this.network.edges[edge]!; // ▸ push the bottleneck along one arc: forward adds flow, reverse cancels it this.tracer?.emit("augment", { amount: bottleneck, capacity: owner.capacity, flow: this.network.flowOn(owner.from, owner.to), from, reverse, to }); } return { nodes, bottleneck }; }}3 augmenting paths carried 23 from source to sink, and the minimum cut has capacity 23: the two agree.