The demo below has a fixed 5-node flow network (left panel) with capacities on each edge. The Gomory-Hu tree built from it is shown on the right. Select a source and a target and the demo will highlight the tree path and report the minimum edge weight on that path — which equals the max-flow between those nodes in the original network.
<p class="hint">Select a <b>source</b> and a <b>target</b> node. The demo highlights
the path in the Gomory-Hu tree and shows the bottleneck edge weight — which
equals the max-flow between those two nodes in the original network.</p>
<div class="panels">
<div class="panel">
<div class="panel-title">Flow Network (capacities)</div>
<svg id="net-svg" width="220" height="220" viewBox="0 0 220 220"></svg>
</div>
<div class="panel">
<div class="panel-title">Gomory-Hu Tree (min-cuts)</div>
<svg id="tree-svg" width="220" height="220" viewBox="0 0 220 220"></svg>
</div>
</div>
<div class="controls">
<label>Source <select id="src"></select></label>
<label>Target <select id="tgt"></select></label>
<button id="query" type="button">Query max-flow</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="result" id="result"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .87rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.panels { display: flex; gap: 10px; flex-wrap: wrap; }
.panel { flex: 1 1 200px; }
.panel-title { font-size: .75rem; font-weight: 700; letter-spacing: .04em;
text-transform: uppercase; color: #1d3557; margin-bottom: 4px; }
svg { background: #f0f4f8; border-radius: 10px; display: block; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin: .6rem 0 .3rem; }
label { font-size: .85rem; display: flex; align-items: center; gap: .3rem; }
select { font: inherit; padding: .22rem .4rem; border: 1px solid #bcc0c6; border-radius: 6px; }
button { font: 600 13px system-ui, sans-serif; padding: .38rem .75rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.result { font-size: .93rem; font-weight: 600; min-height: 1.4em; }
.result.ok { color: #0a7d33; }
.result.idle { color: #555; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Gomory-Hu Tree demo
// 5-node undirected flow network. Nodes: 0=A, 1=B, 2=C, 3=D, 4=E
// Network edges [u, v, capacity]
// Gomory-Hu tree edges [u, v, min-cut-value] computed via Gusfield’s algorithm.
// All 10 pairwise max-flows verified against Ford-Fulkerson ground truth.
var LABELS = ['A','B','C','D','E'];
// Network layout positions (cx, cy) in 220x220 SVG
var NET_POS = [ [110,28],[36,100],[184,100],[60,190],[160,190] ];
// Network edges [u, v, capacity]
var NET_EDGES = [
[0,1,3],[0,2,5],[1,2,2],[1,3,4],[2,4,6],[3,4,2]
];
// Tree layout positions
// Gomory-Hu tree edges [u, v, min-cut value]
// B-A:7, C-A:8, D-B:6, E-C:8
var TREE_EDGES = [
[1,0,7],[2,0,8],[3,1,6],[4,2,8]
];
var N = 5;
// Build tree adjacency
var treeAdj = Array.from({length:N}, function(){ return []; });
TREE_EDGES.forEach(function(e){
treeAdj[e[0]].push({to:e[1],w:e[2]});
treeAdj[e[1]].push({to:e[0],w:e[2]});
});
// BFS path in tree, returns array of {from, to, w} edges or []
function treePath(src, tgt) {
if (src === tgt) return [];
var parent = Array(N).fill(-1);
var parentW = Array(N).fill(0);
var visited = Array(N).fill(false);
var queue = [src]; visited[src] = true;
while (queue.length) {
var u = queue.shift();
for (var i = 0; i < treeAdj[u].length; i++) {
var nb = treeAdj[u][i];
if (!visited[nb.to]) {
visited[nb.to] = true;
parent[nb.to] = u;
parentW[nb.to] = nb.w;
queue.push(nb.to);
}
}
}
var path = [];
var cur = tgt;
while (cur !== src) {
path.unshift({from: parent[cur], to: cur, w: parentW[cur]});
cur = parent[cur];
}
return path;
}
function minOnPath(path) {
var m = Infinity;
path.forEach(function(e){ if (e.w < m) m = e.w; });
return m;
}
// SVG helpers
function svgEl(tag, attrs) {
var el = document.createElementNS('http://www.w3.org/2000/svg', tag);
Object.keys(attrs).forEach(function(k){ el.setAttribute(k, attrs[k]); });
return el;
}
function isHighlightedEdge(u, v, pathEdges) {
for (var i = 0; i < pathEdges.length; i++) {
var e = pathEdges[i];
if ((e.from===u && e.to===v) || (e.from===v && e.to===u)) return true;
}
return false;
}
function drawGraph(svgId, positions, edges, pathEdges, hlSrc, hlTgt) {
var svg = document.getElementById(svgId);
svg.innerHTML = '';
// Draw edges first
edges.forEach(function(e){
var u=e[0],v=e[1],w=e[2];
var x1=positions[u][0],y1=positions[u][1],x2=positions[v][0],y2=positions[v][1];
var hl = isHighlightedEdge(u, v, pathEdges);
svg.appendChild(svgEl('line',{x1:x1,y1:y1,x2:x2,y2:y2,
stroke: hl ? '#e63946' : '#94a8be',
'stroke-width': hl ? 3.2 : 2,
'stroke-linecap': 'round'}));
// Weight label
var mx=(x1+x2)/2, my=(y1+y2)/2;
svg.appendChild(svgEl('rect',{x:mx-9,y:my-8,width:18,height:15,fill:'#f0f4f8',rx:3}));
var lbl = svgEl('text',{x:mx,y:my+4.5,'text-anchor':'middle',
'font-size':'12','font-family':'system-ui,sans-serif',
'font-weight':'700', fill: hl ? '#c92f3c' : '#1d3557'});
lbl.textContent = w;
svg.appendChild(lbl);
});
// Draw nodes
positions.forEach(function(pos, i){
var cx=pos[0], cy=pos[1];
var isSrc = i === hlSrc, isTgt = i === hlTgt;
var fill = isSrc ? '#1d7a4d' : (isTgt ? '#c92f3c' : '#2f5a82');
svg.appendChild(svgEl('circle',{cx:cx,cy:cy,r:17,fill:fill,stroke:'#fff','stroke-width':2.5}));
var lbl = svgEl('text',{x:cx,y:cy+5,'text-anchor':'middle',
'font-size':'13','font-family':'system-ui,sans-serif',
'font-weight':'700',fill:'#fff'});
lbl.textContent = LABELS[i];
svg.appendChild(lbl);
});
}
function populateSelects() {
['src','tgt'].forEach(function(id){
var sel = document.getElementById(id);
LABELS.forEach(function(l,i){
var opt = document.createElement('option');
opt.value = i; opt.textContent = l; sel.appendChild(opt);
});
});
document.getElementById('tgt').value = 3; // default A->D
}
function render(pathEdges, src, tgt) {
drawGraph('net-svg', NET_POS, NET_EDGES, [], src, tgt);
drawGraph('tree-svg', NET_POS, TREE_EDGES.map(function(e){return e.slice();}), pathEdges, src, tgt);
}
function query() {
var src = +document.getElementById('src').value;
var tgt = +document.getElementById('tgt').value;
var res = document.getElementById('result');
if (src === tgt) {
res.textContent = `Source and target are the same node.`;
res.className = 'result idle';
render([], src, tgt);
return;
}
var path = treePath(src, tgt);
var flow = minOnPath(path);
var bottleneck = path.filter(function(e){ return e.w === flow; })[0];
res.textContent = fmt(`max-flow(%v% → %v2%) = %flow% (bottleneck: %v3%–%v4%, weight %flow%)`, { v: LABELS[src], v2: LABELS[tgt], flow: flow, v3: LABELS[bottleneck.from], v4: LABELS[bottleneck.to] });
res.className = 'result ok';
render(path, src, tgt);
console.log(`max-flow`, LABELS[src], '->', LABELS[tgt], '=', flow, `| path edges:`, JSON.stringify(path));
}
function resetView() {
document.getElementById('src').value = 0;
document.getElementById('tgt').value = 3;
document.getElementById('result').textContent = '';
document.getElementById('result').className = 'result idle';
render([], 0, 3);
}
populateSelects();
document.getElementById('query').addEventListener('click', query);
document.getElementById('reset').addEventListener('click', resetView);
render([], 0, 3);
<p class="hint">Select a <b>source</b> and a <b>target</b> node. The demo highlights
the path in the Gomory-Hu tree and shows the bottleneck edge weight — which
equals the max-flow between those two nodes in the original network.</p>
<div class="panels">
<div class="panel">
<div class="panel-title">Flow Network (capacities)</div>
<svg id="net-svg" width="220" height="220" viewBox="0 0 220 220"></svg>
</div>
<div class="panel">
<div class="panel-title">Gomory-Hu Tree (min-cuts)</div>
<svg id="tree-svg" width="220" height="220" viewBox="0 0 220 220"></svg>
</div>
</div>
<div class="controls">
<label>Source <select id="src"></select></label>
<label>Target <select id="tgt"></select></label>
<button id="query" type="button">Query max-flow</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="result" id="result"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .87rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.panels { display: flex; gap: 10px; flex-wrap: wrap; }
.panel { flex: 1 1 200px; }
.panel-title { font-size: .75rem; font-weight: 700; letter-spacing: .04em;
text-transform: uppercase; color: #1d3557; margin-bottom: 4px; }
svg { background: #f0f4f8; border-radius: 10px; display: block; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin: .6rem 0 .3rem; }
label { font-size: .85rem; display: flex; align-items: center; gap: .3rem; }
select { font: inherit; padding: .22rem .4rem; border: 1px solid #bcc0c6; border-radius: 6px; }
button { font: 600 13px system-ui, sans-serif; padding: .38rem .75rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.result { font-size: .93rem; font-weight: 600; min-height: 1.4em; }
.result.ok { color: #0a7d33; }
.result.idle { color: #555; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Gomory-Hu Tree demo
// 5-node undirected flow network. Nodes: 0=A, 1=B, 2=C, 3=D, 4=E
// Network edges [u, v, capacity]
// Gomory-Hu tree edges [u, v, min-cut-value] computed via Gusfield’s algorithm.
// All 10 pairwise max-flows verified against Ford-Fulkerson ground truth.
var LABELS = ['A','B','C','D','E'];
// Network layout positions (cx, cy) in 220x220 SVG
var NET_POS = [ [110,28],[36,100],[184,100],[60,190],[160,190] ];
// Network edges [u, v, capacity]
var NET_EDGES = [
[0,1,3],[0,2,5],[1,2,2],[1,3,4],[2,4,6],[3,4,2]
];
// Tree layout positions
// Gomory-Hu tree edges [u, v, min-cut value]
// B-A:7, C-A:8, D-B:6, E-C:8
var TREE_EDGES = [
[1,0,7],[2,0,8],[3,1,6],[4,2,8]
];
var N = 5;
// Build tree adjacency
var treeAdj = Array.from({length:N}, function(){ return []; });
TREE_EDGES.forEach(function(e){
treeAdj[e[0]].push({to:e[1],w:e[2]});
treeAdj[e[1]].push({to:e[0],w:e[2]});
});
// BFS path in tree, returns array of {from, to, w} edges or []
function treePath(src, tgt) {
if (src === tgt) return [];
var parent = Array(N).fill(-1);
var parentW = Array(N).fill(0);
var visited = Array(N).fill(false);
var queue = [src]; visited[src] = true;
while (queue.length) {
var u = queue.shift();
for (var i = 0; i < treeAdj[u].length; i++) {
var nb = treeAdj[u][i];
if (!visited[nb.to]) {
visited[nb.to] = true;
parent[nb.to] = u;
parentW[nb.to] = nb.w;
queue.push(nb.to);
}
}
}
var path = [];
var cur = tgt;
while (cur !== src) {
path.unshift({from: parent[cur], to: cur, w: parentW[cur]});
cur = parent[cur];
}
return path;
}
function minOnPath(path) {
var m = Infinity;
path.forEach(function(e){ if (e.w < m) m = e.w; });
return m;
}
// SVG helpers
function svgEl(tag, attrs) {
var el = document.createElementNS('http://www.w3.org/2000/svg', tag);
Object.keys(attrs).forEach(function(k){ el.setAttribute(k, attrs[k]); });
return el;
}
function isHighlightedEdge(u, v, pathEdges) {
for (var i = 0; i < pathEdges.length; i++) {
var e = pathEdges[i];
if ((e.from===u && e.to===v) || (e.from===v && e.to===u)) return true;
}
return false;
}
function drawGraph(svgId, positions, edges, pathEdges, hlSrc, hlTgt) {
var svg = document.getElementById(svgId);
svg.innerHTML = '';
// Draw edges first
edges.forEach(function(e){
var u=e[0],v=e[1],w=e[2];
var x1=positions[u][0],y1=positions[u][1],x2=positions[v][0],y2=positions[v][1];
var hl = isHighlightedEdge(u, v, pathEdges);
svg.appendChild(svgEl('line',{x1:x1,y1:y1,x2:x2,y2:y2,
stroke: hl ? '#e63946' : '#94a8be',
'stroke-width': hl ? 3.2 : 2,
'stroke-linecap': 'round'}));
// Weight label
var mx=(x1+x2)/2, my=(y1+y2)/2;
svg.appendChild(svgEl('rect',{x:mx-9,y:my-8,width:18,height:15,fill:'#f0f4f8',rx:3}));
var lbl = svgEl('text',{x:mx,y:my+4.5,'text-anchor':'middle',
'font-size':'12','font-family':'system-ui,sans-serif',
'font-weight':'700', fill: hl ? '#c92f3c' : '#1d3557'});
lbl.textContent = w;
svg.appendChild(lbl);
});
// Draw nodes
positions.forEach(function(pos, i){
var cx=pos[0], cy=pos[1];
var isSrc = i === hlSrc, isTgt = i === hlTgt;
var fill = isSrc ? '#1d7a4d' : (isTgt ? '#c92f3c' : '#2f5a82');
svg.appendChild(svgEl('circle',{cx:cx,cy:cy,r:17,fill:fill,stroke:'#fff','stroke-width':2.5}));
var lbl = svgEl('text',{x:cx,y:cy+5,'text-anchor':'middle',
'font-size':'13','font-family':'system-ui,sans-serif',
'font-weight':'700',fill:'#fff'});
lbl.textContent = LABELS[i];
svg.appendChild(lbl);
});
}
function populateSelects() {
['src','tgt'].forEach(function(id){
var sel = document.getElementById(id);
LABELS.forEach(function(l,i){
var opt = document.createElement('option');
opt.value = i; opt.textContent = l; sel.appendChild(opt);
});
});
document.getElementById('tgt').value = 3; // default A->D
}
function render(pathEdges, src, tgt) {
drawGraph('net-svg', NET_POS, NET_EDGES, [], src, tgt);
drawGraph('tree-svg', NET_POS, TREE_EDGES.map(function(e){return e.slice();}), pathEdges, src, tgt);
}
function query() {
var src = +document.getElementById('src').value;
var tgt = +document.getElementById('tgt').value;
var res = document.getElementById('result');
if (src === tgt) {
res.textContent = `Source and target are the same node.`;
res.className = 'result idle';
render([], src, tgt);
return;
}
var path = treePath(src, tgt);
var flow = minOnPath(path);
var bottleneck = path.filter(function(e){ return e.w === flow; })[0];
res.textContent = fmt(`max-flow(%v% → %v2%) = %flow% (bottleneck: %v3%–%v4%, weight %flow%)`, { v: LABELS[src], v2: LABELS[tgt], flow: flow, v3: LABELS[bottleneck.from], v4: LABELS[bottleneck.to] });
res.className = 'result ok';
render(path, src, tgt);
console.log(`max-flow`, LABELS[src], '->', LABELS[tgt], '=', flow, `| path edges:`, JSON.stringify(path));
}
function resetView() {
document.getElementById('src').value = 0;
document.getElementById('tgt').value = 3;
document.getElementById('result').textContent = '';
document.getElementById('result').className = 'result idle';
render([], 0, 3);
}
populateSelects();
document.getElementById('query').addEventListener('click', query);
document.getElementById('reset').addEventListener('click', resetView);
render([], 0, 3);
Comments
Loading comments...