Below is a small network. Three commodities each need to get from their source to their sink, and every edge can carry only one unit of flow. Choose a route for each commodity; watch the edges they share light up red when they collide.
<p class="hint">Three commodities share one network. Every edge carries <b>one unit</b>.
Click a commodity's <b>Next route</b> to cycle its candidate paths. Edges used by two
commodities turn <b>red</b> (overloaded). Fit all three with no red edge.</p>
<div class="wrap">
<svg id="net" viewBox="0 0 320 220" aria-label="network"></svg>
<div class="panel">
<div class="cmd" data-c="0"><span class="dot d0"></span><b>C1</b>: S1→T1
<button type="button" data-next="0">Next route</button></div>
<div class="cmd" data-c="1"><span class="dot d1"></span><b>C2</b>: S2→T2
<button type="button" data-next="1">Next route</button></div>
<div class="cmd" data-c="2"><span class="dot d2"></span><b>C3</b>: S3→T3
<button type="button" data-next="2">Next route</button></div>
</div>
</div>
<div class="status" id="status">Cycle routes until no edge is red.</div>
<div class="btns">
<button id="solve" type="button">Auto-solve</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.wrap { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-start; }
svg { background: #f3f6f9; border: 1px solid #d6dee6; border-radius: 10px; width: 320px; max-width: 100%; height: auto; }
.panel { display: flex; flex-direction: column; gap: .5rem; min-width: 180px; }
.cmd { font-size: .92rem; display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
.d0 { background: #1d70b8; } .d1 { background: #0a7d33; } .d2 { background: #b8860b; }
.cmd button { font: 600 12px system-ui, sans-serif; padding: .25rem .55rem; border: 1px solid #1d3557;
background: #fff; color: #1d3557; border-radius: 6px; cursor: pointer; }
.edge { stroke: #b7c2cd; stroke-width: 4; stroke-linecap: round; }
.edge.used { stroke-width: 5; }
.edge.u0 { stroke: #1d70b8; } .edge.u1 { stroke: #0a7d33; } .edge.u2 { stroke: #b8860b; }
.edge.over { stroke: #e63946; stroke-width: 6; }
.node circle { fill: #fff; stroke: #5a7088; stroke-width: 1.6; }
.node text { font: 700 9px ui-monospace, monospace; fill: #1d3557; text-anchor: middle; dominant-baseline: central; }
.node.term circle { fill: #e8eef3; }
.status { font-size: 1rem; font-weight: 600; margin: .7rem 0 .5rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; } .status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
.btns button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
.btns button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Multicommodity flow: 3 commodities, one shared network, every edge capacity 1.
// Routing one commodity is easy; choosing WHOLE routes for all so no edge is
// shared is NP-hard in general (integer multicommodity flow; Even, Itai & Shamir, 1976). Here we brute-force the small candidate sets.
const NODES = {
S1:[20,30], S2:[20,110], S3:[20,190],
A:[120,70], B:[120,150], C:[210,70], D:[210,150],
T1:[300,30], T2:[300,110], T3:[300,190]
};
const ek = (u,v) => (u<v? u+'-'+v : v+'-'+u);
const path = (...n) => { const e=[]; for(let i=0;i+1<n.length;i++) e.push(ek(n[i],n[i+1])); return {nodes:n, edges:e}; };
const COMM = [
{ paths:[ path('S1','A','C','T1'), path('S1','B','C','T1'), path('S1','A','D','T1') ] },
{ paths:[ path('S2','A','C','T2'), path('S2','B','D','T2'), path('S2','B','C','T2') ] },
{ paths:[ path('S3','B','D','T3'), path('S3','A','D','T3'), path('S3','B','C','T3') ] },
];
// Master edge list = union of all candidate edges.
const EDGES = (() => { const s=new Set(); COMM.forEach(c=>c.paths.forEach(p=>p.edges.forEach(e=>s.add(e)))); return [...s]; })();
const sel = [0,0,0]; // chosen path index per commodity
function counts(){ const m={}; sel.forEach((pi,ci)=>COMM[ci].paths[pi].edges.forEach(e=>{ (m[e]=m[e]||[]).push(ci); })); return m; }
function drawNet(){
const svg=document.getElementById('net'); svg.innerHTML='';
const used=counts();
// edges
for(const e of EDGES){
const [u,v]=e.split('-'); const [x1,y1]=NODES[u],[x2,y2]=NODES[v];
const l=document.createElementNS('http://www.w3.org/2000/svg','line');
l.setAttribute('x1',x1);l.setAttribute('y1',y1);l.setAttribute('x2',x2);l.setAttribute('y2',y2);
let cls='edge'; const who=used[e];
if(who && who.length>=2) cls+=' used over';
else if(who && who.length===1) cls+=' used u'+who[0];
l.setAttribute('class',cls); svg.appendChild(l);
}
// nodes
for(const n in NODES){ const [x,y]=NODES[n];
const g=document.createElementNS('http://www.w3.org/2000/svg','g');
g.setAttribute('class','node'+(n.length>1?' term':''));
const c=document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',x);c.setAttribute('cy',y);c.setAttribute('r',13);
const t=document.createElementNS('http://www.w3.org/2000/svg','text');
t.setAttribute('x',x);t.setAttribute('y',y);t.textContent=n;
g.appendChild(c);g.appendChild(t);svg.appendChild(g);
}
}
function overloaded(){ const m=counts(); for(const e in m) if(m[e].length>=2) return true; return false; }
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
function refresh(msg){
drawNet();
if(msg!==null){
if(overloaded()) setStatus('At least one edge is overloaded (red). Try other routes.','bad');
else setStatus('✓ All three routed with no shared edge — valid!','ok');
}
}
// Brute force: try every combination of one path per commodity until edge-disjoint.
function solve(){
let tried=0;
for(let a=0;a<COMM[0].paths.length;a++)
for(let b=0;b<COMM[1].paths.length;b++)
for(let c=0;c<COMM[2].paths.length;c++){
tried++;
const m={}; let bad=false;
[a,b,c].forEach((pi,ci)=>COMM[ci].paths[pi].edges.forEach(e=>{ (m[e]=m[e]||0); m[e]++; if(m[e]>1) bad=true; }));
if(!bad){ sel[0]=a;sel[1]=b;sel[2]=c; drawNet();
const total=COMM[0].paths.length*COMM[1].paths.length*COMM[2].paths.length;
setStatus(fmt('Found a conflict-free routing after testing %tried% of %total% combinations.', { tried: tried, total: total }),'ok');
console.log('Routing:', a,b,c,'| tested', tried, 'combos'); return; }
}
setStatus('No combination of whole routes avoids every collision.','bad');
}
document.querySelectorAll('[data-next]').forEach(btn=>btn.addEventListener('click',()=>{
const ci=+btn.dataset.next; sel[ci]=(sel[ci]+1)%COMM[ci].paths.length; refresh(true);
}));
document.getElementById('solve').addEventListener('click',solve);
document.getElementById('reset').addEventListener('click',()=>{ sel[0]=0;sel[1]=0;sel[2]=0; refresh(true); });
refresh(true);
<p class="hint">Three commodities share one network. Every edge carries <b>one unit</b>.
Click a commodity's <b>Next route</b> to cycle its candidate paths. Edges used by two
commodities turn <b>red</b> (overloaded). Fit all three with no red edge.</p>
<div class="wrap">
<svg id="net" viewBox="0 0 320 220" aria-label="network"></svg>
<div class="panel">
<div class="cmd" data-c="0"><span class="dot d0"></span><b>C1</b>: S1→T1
<button type="button" data-next="0">Next route</button></div>
<div class="cmd" data-c="1"><span class="dot d1"></span><b>C2</b>: S2→T2
<button type="button" data-next="1">Next route</button></div>
<div class="cmd" data-c="2"><span class="dot d2"></span><b>C3</b>: S3→T3
<button type="button" data-next="2">Next route</button></div>
</div>
</div>
<div class="status" id="status">Cycle routes until no edge is red.</div>
<div class="btns">
<button id="solve" type="button">Auto-solve</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.wrap { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-start; }
svg { background: #f3f6f9; border: 1px solid #d6dee6; border-radius: 10px; width: 320px; max-width: 100%; height: auto; }
.panel { display: flex; flex-direction: column; gap: .5rem; min-width: 180px; }
.cmd { font-size: .92rem; display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
.d0 { background: #1d70b8; } .d1 { background: #0a7d33; } .d2 { background: #b8860b; }
.cmd button { font: 600 12px system-ui, sans-serif; padding: .25rem .55rem; border: 1px solid #1d3557;
background: #fff; color: #1d3557; border-radius: 6px; cursor: pointer; }
.edge { stroke: #b7c2cd; stroke-width: 4; stroke-linecap: round; }
.edge.used { stroke-width: 5; }
.edge.u0 { stroke: #1d70b8; } .edge.u1 { stroke: #0a7d33; } .edge.u2 { stroke: #b8860b; }
.edge.over { stroke: #e63946; stroke-width: 6; }
.node circle { fill: #fff; stroke: #5a7088; stroke-width: 1.6; }
.node text { font: 700 9px ui-monospace, monospace; fill: #1d3557; text-anchor: middle; dominant-baseline: central; }
.node.term circle { fill: #e8eef3; }
.status { font-size: 1rem; font-weight: 600; margin: .7rem 0 .5rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; } .status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
.btns button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
.btns button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Multicommodity flow: 3 commodities, one shared network, every edge capacity 1.
// Routing one commodity is easy; choosing WHOLE routes for all so no edge is
// shared is NP-hard in general (integer multicommodity flow; Even, Itai & Shamir, 1976). Here we brute-force the small candidate sets.
const NODES = {
S1:[20,30], S2:[20,110], S3:[20,190],
A:[120,70], B:[120,150], C:[210,70], D:[210,150],
T1:[300,30], T2:[300,110], T3:[300,190]
};
const ek = (u,v) => (u<v? u+'-'+v : v+'-'+u);
const path = (...n) => { const e=[]; for(let i=0;i+1<n.length;i++) e.push(ek(n[i],n[i+1])); return {nodes:n, edges:e}; };
const COMM = [
{ paths:[ path('S1','A','C','T1'), path('S1','B','C','T1'), path('S1','A','D','T1') ] },
{ paths:[ path('S2','A','C','T2'), path('S2','B','D','T2'), path('S2','B','C','T2') ] },
{ paths:[ path('S3','B','D','T3'), path('S3','A','D','T3'), path('S3','B','C','T3') ] },
];
// Master edge list = union of all candidate edges.
const EDGES = (() => { const s=new Set(); COMM.forEach(c=>c.paths.forEach(p=>p.edges.forEach(e=>s.add(e)))); return [...s]; })();
const sel = [0,0,0]; // chosen path index per commodity
function counts(){ const m={}; sel.forEach((pi,ci)=>COMM[ci].paths[pi].edges.forEach(e=>{ (m[e]=m[e]||[]).push(ci); })); return m; }
function drawNet(){
const svg=document.getElementById('net'); svg.innerHTML='';
const used=counts();
// edges
for(const e of EDGES){
const [u,v]=e.split('-'); const [x1,y1]=NODES[u],[x2,y2]=NODES[v];
const l=document.createElementNS('http://www.w3.org/2000/svg','line');
l.setAttribute('x1',x1);l.setAttribute('y1',y1);l.setAttribute('x2',x2);l.setAttribute('y2',y2);
let cls='edge'; const who=used[e];
if(who && who.length>=2) cls+=' used over';
else if(who && who.length===1) cls+=' used u'+who[0];
l.setAttribute('class',cls); svg.appendChild(l);
}
// nodes
for(const n in NODES){ const [x,y]=NODES[n];
const g=document.createElementNS('http://www.w3.org/2000/svg','g');
g.setAttribute('class','node'+(n.length>1?' term':''));
const c=document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',x);c.setAttribute('cy',y);c.setAttribute('r',13);
const t=document.createElementNS('http://www.w3.org/2000/svg','text');
t.setAttribute('x',x);t.setAttribute('y',y);t.textContent=n;
g.appendChild(c);g.appendChild(t);svg.appendChild(g);
}
}
function overloaded(){ const m=counts(); for(const e in m) if(m[e].length>=2) return true; return false; }
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
function refresh(msg){
drawNet();
if(msg!==null){
if(overloaded()) setStatus('At least one edge is overloaded (red). Try other routes.','bad');
else setStatus('✓ All three routed with no shared edge — valid!','ok');
}
}
// Brute force: try every combination of one path per commodity until edge-disjoint.
function solve(){
let tried=0;
for(let a=0;a<COMM[0].paths.length;a++)
for(let b=0;b<COMM[1].paths.length;b++)
for(let c=0;c<COMM[2].paths.length;c++){
tried++;
const m={}; let bad=false;
[a,b,c].forEach((pi,ci)=>COMM[ci].paths[pi].edges.forEach(e=>{ (m[e]=m[e]||0); m[e]++; if(m[e]>1) bad=true; }));
if(!bad){ sel[0]=a;sel[1]=b;sel[2]=c; drawNet();
const total=COMM[0].paths.length*COMM[1].paths.length*COMM[2].paths.length;
setStatus(fmt('Found a conflict-free routing after testing %tried% of %total% combinations.', { tried: tried, total: total }),'ok');
console.log('Routing:', a,b,c,'| tested', tried, 'combos'); return; }
}
setStatus('No combination of whole routes avoids every collision.','bad');
}
document.querySelectorAll('[data-next]').forEach(btn=>btn.addEventListener('click',()=>{
const ci=+btn.dataset.next; sel[ci]=(sel[ci]+1)%COMM[ci].paths.length; refresh(true);
}));
document.getElementById('solve').addEventListener('click',solve);
document.getElementById('reset').addEventListener('click',()=>{ sel[0]=0;sel[1]=0;sel[2]=0; refresh(true); });
refresh(true);
Comments
Loading comments...