The demo below shows a six-node flow network (S → A, B → C, D → T) with capacities on each edge. Click Step to advance the algorithm one operation at a time, or Run to animate it to completion. The height of each node is shown inside its circle; orange nodes carry excess flow.
<p class="hint">A six-node flow network. Each edge shows <b>flow / capacity</b>. Orange nodes carry excess. Click <b>Step</b> to run one push or relabel, or <b>Run</b> to animate.</p>
<div class="canvas-wrap"><canvas id="c" width="520" height="300"></canvas></div>
<div class="status" id="status">Ready. Press Step or Run.</div>
<div class="btns">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.canvas-wrap { overflow-x: auto; }
canvas { display: block; border-radius: 10px; background: #f4f7fa; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.done { color: #1d3557; }
.status.active { color: #b86a00; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .42rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Push-Relabel (generic) on a 6-node network: S=0, A=1, B=2, C=3, D=4, T=5
// Edges: [from, to, capacity]
const NODE_LABELS = ['S','A','B','C','D','T'];
const N = 6;
const S = 0, T = 5;
// Positions on 520x300 canvas
const POS = [
[60, 150], // S
[180, 70], // A
[180, 230], // B
[340, 70], // C
[340, 230], // D
[460, 150], // T
];
// Raw edges [from, to, cap]
const RAW_EDGES = [
[S, 1, 15], // S->A cap 15
[S, 2, 10], // S->B cap 10
[1, 3, 12], // A->C cap 12
[1, 2, 4], // A->B cap 4
[2, 4, 10], // B->D cap 10
[3, T, 10], // C->T cap 10
[4, 3, 6], // D->C cap 6
[4, T, 10], // D->T cap 10
];
// Build adjacency list with reverse edges (for residual graph)
// edge object: { to, cap, flow, rev } where rev is index in adj[to]
function buildGraph() {
const adj = Array.from({length: N}, () => []);
function addEdge(u, v, cap) {
adj[u].push({ to: v, cap, flow: 0, rev: adj[v].length });
adj[v].push({ to: u, cap: 0, flow: 0, rev: adj[u].length - 1 });
}
for (const [u, v, c] of RAW_EDGES) addEdge(u, v, c);
return adj;
}
// State
let adj, height, excess, log, animTimer, done;
function initState() {
adj = buildGraph();
height = new Array(N).fill(0);
excess = new Array(N).fill(0);
height[S] = N; // source starts at height N
log = [];
done = false;
// Preflow: saturate all edges from S
for (const e of adj[S]) {
if (e.cap > 0) {
const d = e.cap;
e.flow += d;
adj[e.to][e.rev].flow -= d;
excess[e.to] += d;
excess[S] -= d;
log.push(fmt('Preflow: push %d% units S→%v% (saturate)', { d: d, v: NODE_LABELS[e.to] }));
}
}
}
// One push-relabel step: find first active node (not S, not T, excess>0)
// return description string or null if done
function step() {
// Find active node
let u = -1;
for (let v = 0; v < N; v++) {
if (v !== S && v !== T && excess[v] > 0) { u = v; break; }
}
if (u === -1) return null; // done
// Try to push
for (const e of adj[u]) {
const residual = e.cap - e.flow;
if (residual > 0 && height[u] === height[e.to] + 1) {
const d = Math.min(excess[u], residual);
e.flow += d;
adj[e.to][e.rev].flow -= d;
excess[u] -= d;
excess[e.to] += d;
return fmt('Push %d% units %v%→%v2% (h=%v3%→%v4%)', { d: d, v: NODE_LABELS[u], v2: NODE_LABELS[e.to], v3: height[u], v4: height[e.to] });
}
}
// No push possible: relabel
const oldH = height[u];
let minH = Infinity;
for (const e of adj[u]) {
if (e.cap - e.flow > 0) minH = Math.min(minH, height[e.to]);
}
if (minH === Infinity) {
// Isolated with excess: shouldn't happen in valid flow network but guard it
return fmt('Node %v% isolated (excess %v2% stuck)', { v: NODE_LABELS[u], v2: excess[u] });
}
height[u] = minH + 1;
return fmt('Relabel %v%: h %oldH% → %v2%', { v: NODE_LABELS[u], oldH: oldH, v2: height[u] });
}
// ── Drawing ──────────────────────────────────────────────────────────────────
const COLORS = {
node: '#1d3557',
nodeActive: '#e07b00',
nodeS: '#2a7d4f',
nodeT: '#7b2d8b',
edgeFull: '#c92f3c',
edgePart: '#457b9d',
edgeEmpty: '#cdd9e3',
text: '#fff',
edgeLabel: '#1d3557',
};
function drawArrow(ctx, x1, y1, x2, y2, color, lineWidth) {
const dx = x2 - x1, dy = y2 - y1;
const len = Math.sqrt(dx*dx + dy*dy);
const ux = dx/len, uy = dy/len;
const r = 24; // node radius
const sx = x1 + ux*r, sy = y1 + uy*r;
const ex = x2 - ux*r, ey = y2 - uy*r;
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(ex, ey);
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.stroke();
// arrowhead
const al = 10, aw = 5;
const ax1 = ex - ux*al - uy*aw, ay1 = ey - uy*al + ux*aw;
const ax2 = ex - ux*al + uy*aw, ay2 = ey - uy*al - ux*aw;
ctx.beginPath();
ctx.moveTo(ex, ey);
ctx.lineTo(ax1, ay1);
ctx.lineTo(ax2, ay2);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
function draw() {
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw edges (only forward raw edges for clarity)
ctx.font = '12px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const drawn = new Set();
for (const [u, v] of RAW_EDGES.map(e => [e[0], e[1]])) {
const key = `${u}-${v}`;
if (drawn.has(key)) continue;
drawn.add(key);
// Find the forward edge in adj
let e = adj[u].find(x => x.to === v && x.cap > 0);
if (!e) continue;
const ratio = e.flow / e.cap;
const color = ratio >= 1 ? COLORS.edgeFull : ratio > 0 ? COLORS.edgePart : COLORS.edgeEmpty;
const lw = 1.5 + ratio * 2;
const [x1, y1] = POS[u];
const [x2, y2] = POS[v];
drawArrow(ctx, x1, y1, x2, y2, color, lw);
// Edge label: flow/cap
const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
const nx = -(y2 - y1) / Math.hypot(x2-x1, y2-y1) * 14;
const ny = (x2 - x1) / Math.hypot(x2-x1, y2-y1) * 14;
ctx.fillStyle = COLORS.edgeLabel;
ctx.fillText(`${e.flow}/${e.cap}`, mx + nx, my + ny);
}
// Draw nodes
for (let v = 0; v < N; v++) {
const [x, y] = POS[v];
const isActive = v !== S && v !== T && excess[v] > 0;
const color = v === S ? COLORS.nodeS : v === T ? COLORS.nodeT : isActive ? COLORS.nodeActive : COLORS.node;
ctx.beginPath();
ctx.arc(x, y, 24, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
// Node label
ctx.fillStyle = COLORS.text;
ctx.font = 'bold 14px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(NODE_LABELS[v], x, y - 5);
// Height label
ctx.font = '11px ui-monospace, monospace';
ctx.fillText(`h=${height[v]}`, x, y + 8);
// Excess label (below node)
if (excess[v] !== 0) {
ctx.fillStyle = '#222';
ctx.font = '11px ui-monospace, monospace';
ctx.fillText(`e=${excess[v]}`, x, y + 34);
}
}
// Max-flow value
const flowVal = adj[S].reduce((s, e) => s + Math.max(0, e.flow), 0);
ctx.fillStyle = '#1d3557';
ctx.font = 'bold 13px system-ui, sans-serif';
ctx.textAlign = 'left';
ctx.fillText(fmt('Max flow so far: %flowVal%', { flowVal: flowVal }), 8, 16);
}
function setStatus(t, cls) {
const el = document.getElementById('status');
el.textContent = t;
el.className = 'status ' + (cls || '');
}
function doStep() {
if (done) return;
const msg = step();
if (msg === null) {
const flowVal = adj[S].reduce((s, e) => s + Math.max(0, e.flow), 0);
setStatus(fmt('Done! Maximum flow = %flowVal%', { flowVal: flowVal }), 'ok');
done = true;
document.getElementById('btn-step').disabled = true;
document.getElementById('btn-run').disabled = true;
} else {
setStatus(msg, 'active');
}
draw();
}
function runAnim() {
if (done) return;
document.getElementById('btn-step').disabled = true;
document.getElementById('btn-run').disabled = true;
animTimer = setInterval(() => {
doStep();
if (done) clearInterval(animTimer);
}, 500);
}
function reset() {
clearInterval(animTimer);
initState();
done = false;
document.getElementById('btn-step').disabled = false;
document.getElementById('btn-run').disabled = false;
setStatus('Ready. Press Step or Run.', '');
draw();
}
document.getElementById('btn-step').addEventListener('click', doStep);
document.getElementById('btn-run').addEventListener('click', runAnim);
document.getElementById('btn-reset').addEventListener('click', reset);
// Boot
initState();
draw();
// Verify: run full algorithm and log result
(function verify() {
const savedAdj = adj, savedH = height, savedE = excess, savedDone = done;
initState();
let iterations = 0;
while (true) {
const msg = step();
if (msg === null) break;
if (++iterations > 10000) { console.error('Push-relabel: too many iterations'); break; }
}
const flowVal = adj[S].reduce((s, e) => s + Math.max(0, e.flow), 0);
console.log('Push-relabel verification: max flow =', flowVal, '(expected 20)');
console.assert(flowVal === 20, 'Max flow should be 20');
adj = savedAdj; height = savedH; excess = savedE; done = savedDone;
initState(); draw();
})();
<p class="hint">A six-node flow network. Each edge shows <b>flow / capacity</b>. Orange nodes carry excess. Click <b>Step</b> to run one push or relabel, or <b>Run</b> to animate.</p>
<div class="canvas-wrap"><canvas id="c" width="520" height="300"></canvas></div>
<div class="status" id="status">Ready. Press Step or Run.</div>
<div class="btns">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.canvas-wrap { overflow-x: auto; }
canvas { display: block; border-radius: 10px; background: #f4f7fa; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.done { color: #1d3557; }
.status.active { color: #b86a00; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .42rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Push-Relabel (generic) on a 6-node network: S=0, A=1, B=2, C=3, D=4, T=5
// Edges: [from, to, capacity]
const NODE_LABELS = ['S','A','B','C','D','T'];
const N = 6;
const S = 0, T = 5;
// Positions on 520x300 canvas
const POS = [
[60, 150], // S
[180, 70], // A
[180, 230], // B
[340, 70], // C
[340, 230], // D
[460, 150], // T
];
// Raw edges [from, to, cap]
const RAW_EDGES = [
[S, 1, 15], // S->A cap 15
[S, 2, 10], // S->B cap 10
[1, 3, 12], // A->C cap 12
[1, 2, 4], // A->B cap 4
[2, 4, 10], // B->D cap 10
[3, T, 10], // C->T cap 10
[4, 3, 6], // D->C cap 6
[4, T, 10], // D->T cap 10
];
// Build adjacency list with reverse edges (for residual graph)
// edge object: { to, cap, flow, rev } where rev is index in adj[to]
function buildGraph() {
const adj = Array.from({length: N}, () => []);
function addEdge(u, v, cap) {
adj[u].push({ to: v, cap, flow: 0, rev: adj[v].length });
adj[v].push({ to: u, cap: 0, flow: 0, rev: adj[u].length - 1 });
}
for (const [u, v, c] of RAW_EDGES) addEdge(u, v, c);
return adj;
}
// State
let adj, height, excess, log, animTimer, done;
function initState() {
adj = buildGraph();
height = new Array(N).fill(0);
excess = new Array(N).fill(0);
height[S] = N; // source starts at height N
log = [];
done = false;
// Preflow: saturate all edges from S
for (const e of adj[S]) {
if (e.cap > 0) {
const d = e.cap;
e.flow += d;
adj[e.to][e.rev].flow -= d;
excess[e.to] += d;
excess[S] -= d;
log.push(fmt('Preflow: push %d% units S→%v% (saturate)', { d: d, v: NODE_LABELS[e.to] }));
}
}
}
// One push-relabel step: find first active node (not S, not T, excess>0)
// return description string or null if done
function step() {
// Find active node
let u = -1;
for (let v = 0; v < N; v++) {
if (v !== S && v !== T && excess[v] > 0) { u = v; break; }
}
if (u === -1) return null; // done
// Try to push
for (const e of adj[u]) {
const residual = e.cap - e.flow;
if (residual > 0 && height[u] === height[e.to] + 1) {
const d = Math.min(excess[u], residual);
e.flow += d;
adj[e.to][e.rev].flow -= d;
excess[u] -= d;
excess[e.to] += d;
return fmt('Push %d% units %v%→%v2% (h=%v3%→%v4%)', { d: d, v: NODE_LABELS[u], v2: NODE_LABELS[e.to], v3: height[u], v4: height[e.to] });
}
}
// No push possible: relabel
const oldH = height[u];
let minH = Infinity;
for (const e of adj[u]) {
if (e.cap - e.flow > 0) minH = Math.min(minH, height[e.to]);
}
if (minH === Infinity) {
// Isolated with excess: shouldn't happen in valid flow network but guard it
return fmt('Node %v% isolated (excess %v2% stuck)', { v: NODE_LABELS[u], v2: excess[u] });
}
height[u] = minH + 1;
return fmt('Relabel %v%: h %oldH% → %v2%', { v: NODE_LABELS[u], oldH: oldH, v2: height[u] });
}
// ── Drawing ──────────────────────────────────────────────────────────────────
const COLORS = {
node: '#1d3557',
nodeActive: '#e07b00',
nodeS: '#2a7d4f',
nodeT: '#7b2d8b',
edgeFull: '#c92f3c',
edgePart: '#457b9d',
edgeEmpty: '#cdd9e3',
text: '#fff',
edgeLabel: '#1d3557',
};
function drawArrow(ctx, x1, y1, x2, y2, color, lineWidth) {
const dx = x2 - x1, dy = y2 - y1;
const len = Math.sqrt(dx*dx + dy*dy);
const ux = dx/len, uy = dy/len;
const r = 24; // node radius
const sx = x1 + ux*r, sy = y1 + uy*r;
const ex = x2 - ux*r, ey = y2 - uy*r;
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(ex, ey);
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.stroke();
// arrowhead
const al = 10, aw = 5;
const ax1 = ex - ux*al - uy*aw, ay1 = ey - uy*al + ux*aw;
const ax2 = ex - ux*al + uy*aw, ay2 = ey - uy*al - ux*aw;
ctx.beginPath();
ctx.moveTo(ex, ey);
ctx.lineTo(ax1, ay1);
ctx.lineTo(ax2, ay2);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
function draw() {
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw edges (only forward raw edges for clarity)
ctx.font = '12px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const drawn = new Set();
for (const [u, v] of RAW_EDGES.map(e => [e[0], e[1]])) {
const key = `${u}-${v}`;
if (drawn.has(key)) continue;
drawn.add(key);
// Find the forward edge in adj
let e = adj[u].find(x => x.to === v && x.cap > 0);
if (!e) continue;
const ratio = e.flow / e.cap;
const color = ratio >= 1 ? COLORS.edgeFull : ratio > 0 ? COLORS.edgePart : COLORS.edgeEmpty;
const lw = 1.5 + ratio * 2;
const [x1, y1] = POS[u];
const [x2, y2] = POS[v];
drawArrow(ctx, x1, y1, x2, y2, color, lw);
// Edge label: flow/cap
const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
const nx = -(y2 - y1) / Math.hypot(x2-x1, y2-y1) * 14;
const ny = (x2 - x1) / Math.hypot(x2-x1, y2-y1) * 14;
ctx.fillStyle = COLORS.edgeLabel;
ctx.fillText(`${e.flow}/${e.cap}`, mx + nx, my + ny);
}
// Draw nodes
for (let v = 0; v < N; v++) {
const [x, y] = POS[v];
const isActive = v !== S && v !== T && excess[v] > 0;
const color = v === S ? COLORS.nodeS : v === T ? COLORS.nodeT : isActive ? COLORS.nodeActive : COLORS.node;
ctx.beginPath();
ctx.arc(x, y, 24, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
// Node label
ctx.fillStyle = COLORS.text;
ctx.font = 'bold 14px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(NODE_LABELS[v], x, y - 5);
// Height label
ctx.font = '11px ui-monospace, monospace';
ctx.fillText(`h=${height[v]}`, x, y + 8);
// Excess label (below node)
if (excess[v] !== 0) {
ctx.fillStyle = '#222';
ctx.font = '11px ui-monospace, monospace';
ctx.fillText(`e=${excess[v]}`, x, y + 34);
}
}
// Max-flow value
const flowVal = adj[S].reduce((s, e) => s + Math.max(0, e.flow), 0);
ctx.fillStyle = '#1d3557';
ctx.font = 'bold 13px system-ui, sans-serif';
ctx.textAlign = 'left';
ctx.fillText(fmt('Max flow so far: %flowVal%', { flowVal: flowVal }), 8, 16);
}
function setStatus(t, cls) {
const el = document.getElementById('status');
el.textContent = t;
el.className = 'status ' + (cls || '');
}
function doStep() {
if (done) return;
const msg = step();
if (msg === null) {
const flowVal = adj[S].reduce((s, e) => s + Math.max(0, e.flow), 0);
setStatus(fmt('Done! Maximum flow = %flowVal%', { flowVal: flowVal }), 'ok');
done = true;
document.getElementById('btn-step').disabled = true;
document.getElementById('btn-run').disabled = true;
} else {
setStatus(msg, 'active');
}
draw();
}
function runAnim() {
if (done) return;
document.getElementById('btn-step').disabled = true;
document.getElementById('btn-run').disabled = true;
animTimer = setInterval(() => {
doStep();
if (done) clearInterval(animTimer);
}, 500);
}
function reset() {
clearInterval(animTimer);
initState();
done = false;
document.getElementById('btn-step').disabled = false;
document.getElementById('btn-run').disabled = false;
setStatus('Ready. Press Step or Run.', '');
draw();
}
document.getElementById('btn-step').addEventListener('click', doStep);
document.getElementById('btn-run').addEventListener('click', runAnim);
document.getElementById('btn-reset').addEventListener('click', reset);
// Boot
initState();
draw();
// Verify: run full algorithm and log result
(function verify() {
const savedAdj = adj, savedH = height, savedE = excess, savedDone = done;
initState();
let iterations = 0;
while (true) {
const msg = step();
if (msg === null) break;
if (++iterations > 10000) { console.error('Push-relabel: too many iterations'); break; }
}
const flowVal = adj[S].reduce((s, e) => s + Math.max(0, e.flow), 0);
console.log('Push-relabel verification: max flow =', flowVal, '(expected 20)');
console.assert(flowVal === 20, 'Max flow should be 20');
adj = savedAdj; height = savedH; excess = savedE; done = savedDone;
initState(); draw();
})();
Watch how excess accumulates near the source before gradually flowing — or being pushed back — through the network. Each relabel raises a node's height when it has no downhill neighbor to receive its excess. The algorithm terminates when only S and T carry non-zero flow balance.
Comments
Loading comments...