Below is a small supply-and-demand network. Green nodes are sources (positive supply), red nodes are sinks (positive demand), and the edges carry unit costs. The thick blue edges form the current spanning tree basis.
<p class="hint">A supply-demand network with edge costs. <b>Blue edges</b> form the spanning-tree basis. Press <b>Next pivot</b> to run one step of network simplex โ it finds a negative-reduced-cost arc, adds it to the tree, and removes one arc from the resulting cycle.</p>
<canvas id="canvas" width="420" height="265"></canvas>
<div class="info" id="info">Reduced costs shown on dashed non-tree edges. Red = negative (can improve).</div>
<div class="btns">
<button id="pivot" type="button">Next pivot</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="legend">
<span class="leg-item"><span class="dot supply"></span> Supply (+3)</span>
<span class="leg-item"><span class="dot demand"></span> Demand (โ3)</span>
<span class="leg-item"><span class="dot neutral"></span> Transit (0)</span>
</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 .55rem; line-height: 1.45; }
canvas { display: block; border-radius: 10px; background: #f4f7fa;
border: 1px solid #dce3ea; max-width: 100%; }
.info { font-size: .88rem; font-weight: 600; margin: .45rem 0; min-height: 1.3em; color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .4rem; }
button { font: 600 14px system-ui; padding: .42rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .42; cursor: default; }
.legend { display: flex; gap: .9rem; flex-wrap: wrap; font-size: .8rem; color: #555; }
.leg-item { display: flex; align-items: center; gap: .3rem; }
.dot { width: 11px; height: 11px; border-radius: 50%; display: inline-block; border: 1.5px solid #888; }
.dot.supply { background: #2e7d32; border-color: #1b5e20; }
.dot.demand { background: #c62828; border-color: #7f0000; }
.dot.neutral { background: #e8eef3; border-color: #8898a9; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Network simplex demo โ 5-node min-cost flow instance.
// Nodes: S(supply=3), A(transit), B(transit), C(transit), T(demand=3).
// Initial spanning tree routes all flow through the expensive SโAโCโT path (cost 15).
// Two pivots bring it to the optimal SโBโCโT path (cost 12).
// โโ Graph โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const NODES = [
{ id:0, label:'S', b:3, x:52, y:132, type:'supply' },
{ id:1, label:'A', b:0, x:165, y:55, type:'neutral' },
{ id:2, label:'B', b:0, x:165, y:210, type:'neutral' },
{ id:3, label:'C', b:0, x:290, y:132, type:'neutral' },
{ id:4, label:'T', b:-3, x:385, y:132, type:'demand' }
];
// 6 directed arcs with capacities and costs
const ARCS = [
{ from:0, to:1, cap:5, cost:3 }, // 0: SโA cost 3
{ from:0, to:2, cap:5, cost:1 }, // 1: SโB cost 1
{ from:1, to:3, cap:5, cost:1 }, // 2: AโC cost 1
{ from:2, to:3, cap:5, cost:2 }, // 3: BโC cost 2
{ from:1, to:2, cap:5, cost:3 }, // 4: AโB cost 3
{ from:3, to:4, cap:5, cost:1 }, // 5: CโT cost 1
];
// โโ State โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let treeSet, flow, optimal, pivotCount;
function initState() {
// Initial tree: {0,2,4,5} SโA, AโC, AโB(leaf), CโT
// feasible flow: 3 units on SโAโCโT, 0 elsewhere; cost = 3*3+3*1+3*1 = 15
treeSet = new Set([0, 2, 4, 5]);
flow = [3, 0, 3, 0, 0, 3];
optimal = false;
pivotCount = 0;
}
// โโ Dual: node potentials from spanning tree โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// For each tree arc (iโj): reduced_cost = cost - ฯ[i] + ฯ[j] = 0
// So ฯ[j] = ฯ[i] - cost. Set ฯ[root=0] = 0 and propagate.
function computePotentials() {
const pi = new Array(NODES.length).fill(null);
pi[0] = 0;
let changed = true;
while (changed) {
changed = false;
for (const ei of treeSet) {
const e = ARCS[ei];
if (pi[e.from] !== null && pi[e.to] === null) { pi[e.to] = pi[e.from] - e.cost; changed = true; }
if (pi[e.to] !== null && pi[e.from] === null) { pi[e.from] = pi[e.to] + e.cost; changed = true; }
}
}
return pi;
}
// Reduced cost of arc ei: cฬ = cost - ฯ[from] + ฯ[to]
function rc(ei, pi) { const e = ARCS[ei]; return e.cost - pi[e.from] + pi[e.to]; }
function totalCost() { return ARCS.reduce((s, e, i) => s + e.cost * flow[i], 0); }
// โโ Pivot โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 1. Find non-tree arc with most-negative reduced cost (entering arc).
// 2. Add it to tree โ creates exactly one cycle.
// 3. Find cycle via BFS in the tree.
// 4. Send as much flow as possible around the cycle.
// 5. The arc that hits its bound (capacity or 0) leaves the tree.
function pivot() {
const pi = computePotentials();
// Entering arc: most-negative reduced cost
let enterIdx = -1, bestRC = 0;
for (let ei = 0; ei < ARCS.length; ei++) {
if (treeSet.has(ei)) continue;
const r = rc(ei, pi);
if (r < bestRC) { bestRC = r; enterIdx = ei; }
}
if (enterIdx === -1) { optimal = true; return null; }
const enter = ARCS[enterIdx];
// BFS tree to find path from enter.from to enter.to
const adj = Array.from({ length: NODES.length }, () => []);
for (const ei of treeSet) {
const e = ARCS[ei];
adj[e.from].push({ node: e.to, ei, fwd: true });
adj[e.to ].push({ node: e.from, ei, fwd: false });
}
const parent = new Array(NODES.length).fill(null);
const visited = new Array(NODES.length).fill(false);
const queue = [enter.from]; visited[enter.from] = true;
let found = false;
while (queue.length && !found) {
const u = queue.shift();
for (const { node: v, ei, fwd } of adj[u]) {
if (!visited[v]) {
visited[v] = true;
parent[v] = { node: u, ei, fwd };
if (v === enter.to) { found = true; break; }
queue.push(v);
}
}
}
// Build cycle: trace path from enter.to back to enter.from (direction reversed)
// parent[v]={node:u, fwd:true} means tree arc goes uโv, so cycle goes vโu (reversed)
const cycle = [];
let cur = enter.to;
while (cur !== enter.from) {
const p = parent[cur];
cycle.push({ ei: p.ei, fwd: !p.fwd }); // reverse direction for return path
cur = p.node;
}
cycle.push({ ei: enterIdx, fwd: true }); // entering arc closes the cycle forward
// Max flow delta = min residual capacity around cycle
// fwd arc: residual = cap - flow; rev arc: residual = flow
let delta = Infinity, leaveIdx = -1;
for (const { ei, fwd } of cycle) {
if (ei === enterIdx) continue;
const residual = fwd ? (ARCS[ei].cap - flow[ei]) : flow[ei];
if (residual < delta) { delta = residual; leaveIdx = ei; }
}
if (delta === Infinity || leaveIdx === -1) { optimal = true; return null; } // degenerate guard
// Update flows
for (const { ei, fwd } of cycle) {
if (fwd) flow[ei] += delta; else flow[ei] -= delta;
}
// Swap tree arcs
treeSet.delete(leaveIdx);
treeSet.add(enterIdx);
pivotCount++;
return {
enterIdx, leaveIdx, delta, rc: bestRC,
enterName: NODES[enter.from].label + 'โ' + NODES[enter.to].label,
leaveName: NODES[ARCS[leaveIdx].from].label + 'โ' + NODES[ARCS[leaveIdx].to].label
};
}
// โโ Canvas drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Slight per-arc offset so parallel edges don't overlap
const ARC_OFFSET = [0, 18, 0, -18, 0, 0]; // px perpendicular offset per arc
function arrowHead(x1, y1, x2, y2, color) {
const dx = x2 - x1, dy = y2 - y1;
const len = Math.sqrt(dx*dx + dy*dy);
const ux = dx/len, uy = dy/len;
const px = -uy, py = ux;
const tip = { x: x2 - ux*16, y: y2 - uy*16 };
ctx.beginPath();
ctx.moveTo(tip.x - ux*2 + px*5, tip.y - uy*2 + py*5);
ctx.lineTo(tip.x + ux*4, tip.y + uy*4);
ctx.lineTo(tip.x - ux*2 - px*5, tip.y - uy*2 - py*5);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
function draw() {
ctx.clearRect(0, 0, W, H);
const pi = computePotentials();
// Draw arcs
for (let ei = 0; ei < ARCS.length; ei++) {
const e = ARCS[ei];
const fn = NODES[e.from], tn = NODES[e.to];
const inTree = treeSet.has(ei);
const rCost = rc(ei, pi);
const off = ARC_OFFSET[ei];
// Perpendicular offset
const dx = tn.x - fn.x, dy = tn.y - fn.y;
const len = Math.sqrt(dx*dx + dy*dy);
const ox = -dy/len * off, oy = dx/len * off;
const x1 = fn.x + ox, y1 = fn.y + oy;
const x2 = tn.x + ox, y2 = tn.y + oy;
const color = inTree ? '#1565c0' : (rCost < 0 ? '#c62828' : '#9aaebc');
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.lineWidth = inTree ? 2.8 : 1.6;
ctx.strokeStyle = color;
ctx.setLineDash(inTree ? [] : [5, 4]);
ctx.stroke();
ctx.setLineDash([]);
arrowHead(x1, y1, x2, y2, color);
// Label: cost (and flow for tree arcs, reduced cost for non-tree)
const mx = (x1 + x2)/2, my = (y1 + y2)/2;
const lx = mx + (-dy/len)*12, ly = my + (dx/len)*12; // push label sideways
ctx.font = '10.5px ui-monospace, monospace';
ctx.fillStyle = inTree ? '#0d47a1' : (rCost < 0 ? '#b71c1c' : '#546e7a');
const lbl = inTree ? `c${e.cost} f${flow[ei]}` : `c${e.cost} r${rCost}`;
ctx.textAlign = 'center';
ctx.fillText(lbl, lx, ly);
}
ctx.textAlign = 'left';
// Draw nodes
for (const n of NODES) {
ctx.beginPath();
ctx.arc(n.x, n.y, 19, 0, 2*Math.PI);
ctx.fillStyle = n.type==='supply' ? '#2e7d32' : n.type==='demand' ? '#c62828' : '#e8eef3';
ctx.fill();
ctx.strokeStyle = n.type==='supply' ? '#1b5e20' : n.type==='demand' ? '#7f0000' : '#8898a9';
ctx.lineWidth = 2;
ctx.stroke();
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = 'bold 13px system-ui';
ctx.fillStyle = n.type==='neutral' ? '#1d3557' : '#fff';
ctx.fillText(n.label, n.x, n.y - 2);
ctx.font = '10px system-ui';
ctx.fillStyle = n.type==='neutral' ? '#78909c' : '#cfd8dc';
ctx.fillText((n.b > 0 ? '+' : '') + n.b, n.x, n.y + 8);
}
ctx.textAlign = 'left';
ctx.textBaseline = 'alphabetic';
}
// โโ UI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const info = document.getElementById('info');
const pivotBtn = document.getElementById('pivot');
function setInfo(msg) { info.textContent = msg; }
pivotBtn.addEventListener('click', () => {
const result = pivot();
draw();
if (!result) {
setInfo(fmt('Optimal! All reduced costs โฅ 0. Total cost = %totalCost% after %pivotCount% pivot(s).', { totalCost: totalCost(), pivotCount: pivotCount }));
pivotBtn.disabled = true;
} else {
const degStr = result.delta === 0 ? ' (degenerate pivot, ฮ=0)' : '';
setInfo(fmt('Pivot %pivotCount%: arc %enterName% IN,, arc %leaveName% OUT (ฮ=%delta%, rc=%rc%)%degStr%. Cost = %totalCost%.', { pivotCount: pivotCount, enterName: result.enterName, leaveName: result.leaveName, delta: result.delta, rc: result.rc, degStr: degStr, totalCost: totalCost() }));
}
});
document.getElementById('reset').addEventListener('click', () => {
initState();
pivotBtn.disabled = false;
setInfo('Reduced costs shown on dashed non-tree edges. Red = negative (can improve).');
draw();
});
// โโ Boot & verification โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
initState();
draw();
// Verify algorithm offline: run to completion, check optimal cost = 12
(function verify() {
const savedTree = new Set(treeSet);
const savedFlow = [...flow];
const savedOptimal = optimal;
const savedPivotCount = pivotCount;
initState();
let steps = 0;
while (steps < 20) {
const r = pivot();
if (!r) break;
steps++;
}
const optCost = totalCost();
// Optimal: route all 3 units via SโBโCโT at cost 1+2+1=4/unit โ total 12
console.assert(optCost === 12, fmt('Optimal cost should be 12, got %optCost%', { optCost: optCost }));
console.assert(optimal === true, 'Should have reached optimal');
console.log('Verification: optimal cost =', optCost, '(expected 12) after', steps, 'pivots โ');
// Restore initial display state
treeSet = savedTree;
flow = savedFlow;
optimal = savedOptimal;
pivotCount = savedPivotCount;
draw();
})();
<p class="hint">A supply-demand network with edge costs. <b>Blue edges</b> form the spanning-tree basis. Press <b>Next pivot</b> to run one step of network simplex โ it finds a negative-reduced-cost arc, adds it to the tree, and removes one arc from the resulting cycle.</p>
<canvas id="canvas" width="420" height="265"></canvas>
<div class="info" id="info">Reduced costs shown on dashed non-tree edges. Red = negative (can improve).</div>
<div class="btns">
<button id="pivot" type="button">Next pivot</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="legend">
<span class="leg-item"><span class="dot supply"></span> Supply (+3)</span>
<span class="leg-item"><span class="dot demand"></span> Demand (โ3)</span>
<span class="leg-item"><span class="dot neutral"></span> Transit (0)</span>
</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 .55rem; line-height: 1.45; }
canvas { display: block; border-radius: 10px; background: #f4f7fa;
border: 1px solid #dce3ea; max-width: 100%; }
.info { font-size: .88rem; font-weight: 600; margin: .45rem 0; min-height: 1.3em; color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .4rem; }
button { font: 600 14px system-ui; padding: .42rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .42; cursor: default; }
.legend { display: flex; gap: .9rem; flex-wrap: wrap; font-size: .8rem; color: #555; }
.leg-item { display: flex; align-items: center; gap: .3rem; }
.dot { width: 11px; height: 11px; border-radius: 50%; display: inline-block; border: 1.5px solid #888; }
.dot.supply { background: #2e7d32; border-color: #1b5e20; }
.dot.demand { background: #c62828; border-color: #7f0000; }
.dot.neutral { background: #e8eef3; border-color: #8898a9; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Network simplex demo โ 5-node min-cost flow instance.
// Nodes: S(supply=3), A(transit), B(transit), C(transit), T(demand=3).
// Initial spanning tree routes all flow through the expensive SโAโCโT path (cost 15).
// Two pivots bring it to the optimal SโBโCโT path (cost 12).
// โโ Graph โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const NODES = [
{ id:0, label:'S', b:3, x:52, y:132, type:'supply' },
{ id:1, label:'A', b:0, x:165, y:55, type:'neutral' },
{ id:2, label:'B', b:0, x:165, y:210, type:'neutral' },
{ id:3, label:'C', b:0, x:290, y:132, type:'neutral' },
{ id:4, label:'T', b:-3, x:385, y:132, type:'demand' }
];
// 6 directed arcs with capacities and costs
const ARCS = [
{ from:0, to:1, cap:5, cost:3 }, // 0: SโA cost 3
{ from:0, to:2, cap:5, cost:1 }, // 1: SโB cost 1
{ from:1, to:3, cap:5, cost:1 }, // 2: AโC cost 1
{ from:2, to:3, cap:5, cost:2 }, // 3: BโC cost 2
{ from:1, to:2, cap:5, cost:3 }, // 4: AโB cost 3
{ from:3, to:4, cap:5, cost:1 }, // 5: CโT cost 1
];
// โโ State โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let treeSet, flow, optimal, pivotCount;
function initState() {
// Initial tree: {0,2,4,5} SโA, AโC, AโB(leaf), CโT
// feasible flow: 3 units on SโAโCโT, 0 elsewhere; cost = 3*3+3*1+3*1 = 15
treeSet = new Set([0, 2, 4, 5]);
flow = [3, 0, 3, 0, 0, 3];
optimal = false;
pivotCount = 0;
}
// โโ Dual: node potentials from spanning tree โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// For each tree arc (iโj): reduced_cost = cost - ฯ[i] + ฯ[j] = 0
// So ฯ[j] = ฯ[i] - cost. Set ฯ[root=0] = 0 and propagate.
function computePotentials() {
const pi = new Array(NODES.length).fill(null);
pi[0] = 0;
let changed = true;
while (changed) {
changed = false;
for (const ei of treeSet) {
const e = ARCS[ei];
if (pi[e.from] !== null && pi[e.to] === null) { pi[e.to] = pi[e.from] - e.cost; changed = true; }
if (pi[e.to] !== null && pi[e.from] === null) { pi[e.from] = pi[e.to] + e.cost; changed = true; }
}
}
return pi;
}
// Reduced cost of arc ei: cฬ = cost - ฯ[from] + ฯ[to]
function rc(ei, pi) { const e = ARCS[ei]; return e.cost - pi[e.from] + pi[e.to]; }
function totalCost() { return ARCS.reduce((s, e, i) => s + e.cost * flow[i], 0); }
// โโ Pivot โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 1. Find non-tree arc with most-negative reduced cost (entering arc).
// 2. Add it to tree โ creates exactly one cycle.
// 3. Find cycle via BFS in the tree.
// 4. Send as much flow as possible around the cycle.
// 5. The arc that hits its bound (capacity or 0) leaves the tree.
function pivot() {
const pi = computePotentials();
// Entering arc: most-negative reduced cost
let enterIdx = -1, bestRC = 0;
for (let ei = 0; ei < ARCS.length; ei++) {
if (treeSet.has(ei)) continue;
const r = rc(ei, pi);
if (r < bestRC) { bestRC = r; enterIdx = ei; }
}
if (enterIdx === -1) { optimal = true; return null; }
const enter = ARCS[enterIdx];
// BFS tree to find path from enter.from to enter.to
const adj = Array.from({ length: NODES.length }, () => []);
for (const ei of treeSet) {
const e = ARCS[ei];
adj[e.from].push({ node: e.to, ei, fwd: true });
adj[e.to ].push({ node: e.from, ei, fwd: false });
}
const parent = new Array(NODES.length).fill(null);
const visited = new Array(NODES.length).fill(false);
const queue = [enter.from]; visited[enter.from] = true;
let found = false;
while (queue.length && !found) {
const u = queue.shift();
for (const { node: v, ei, fwd } of adj[u]) {
if (!visited[v]) {
visited[v] = true;
parent[v] = { node: u, ei, fwd };
if (v === enter.to) { found = true; break; }
queue.push(v);
}
}
}
// Build cycle: trace path from enter.to back to enter.from (direction reversed)
// parent[v]={node:u, fwd:true} means tree arc goes uโv, so cycle goes vโu (reversed)
const cycle = [];
let cur = enter.to;
while (cur !== enter.from) {
const p = parent[cur];
cycle.push({ ei: p.ei, fwd: !p.fwd }); // reverse direction for return path
cur = p.node;
}
cycle.push({ ei: enterIdx, fwd: true }); // entering arc closes the cycle forward
// Max flow delta = min residual capacity around cycle
// fwd arc: residual = cap - flow; rev arc: residual = flow
let delta = Infinity, leaveIdx = -1;
for (const { ei, fwd } of cycle) {
if (ei === enterIdx) continue;
const residual = fwd ? (ARCS[ei].cap - flow[ei]) : flow[ei];
if (residual < delta) { delta = residual; leaveIdx = ei; }
}
if (delta === Infinity || leaveIdx === -1) { optimal = true; return null; } // degenerate guard
// Update flows
for (const { ei, fwd } of cycle) {
if (fwd) flow[ei] += delta; else flow[ei] -= delta;
}
// Swap tree arcs
treeSet.delete(leaveIdx);
treeSet.add(enterIdx);
pivotCount++;
return {
enterIdx, leaveIdx, delta, rc: bestRC,
enterName: NODES[enter.from].label + 'โ' + NODES[enter.to].label,
leaveName: NODES[ARCS[leaveIdx].from].label + 'โ' + NODES[ARCS[leaveIdx].to].label
};
}
// โโ Canvas drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Slight per-arc offset so parallel edges don't overlap
const ARC_OFFSET = [0, 18, 0, -18, 0, 0]; // px perpendicular offset per arc
function arrowHead(x1, y1, x2, y2, color) {
const dx = x2 - x1, dy = y2 - y1;
const len = Math.sqrt(dx*dx + dy*dy);
const ux = dx/len, uy = dy/len;
const px = -uy, py = ux;
const tip = { x: x2 - ux*16, y: y2 - uy*16 };
ctx.beginPath();
ctx.moveTo(tip.x - ux*2 + px*5, tip.y - uy*2 + py*5);
ctx.lineTo(tip.x + ux*4, tip.y + uy*4);
ctx.lineTo(tip.x - ux*2 - px*5, tip.y - uy*2 - py*5);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
function draw() {
ctx.clearRect(0, 0, W, H);
const pi = computePotentials();
// Draw arcs
for (let ei = 0; ei < ARCS.length; ei++) {
const e = ARCS[ei];
const fn = NODES[e.from], tn = NODES[e.to];
const inTree = treeSet.has(ei);
const rCost = rc(ei, pi);
const off = ARC_OFFSET[ei];
// Perpendicular offset
const dx = tn.x - fn.x, dy = tn.y - fn.y;
const len = Math.sqrt(dx*dx + dy*dy);
const ox = -dy/len * off, oy = dx/len * off;
const x1 = fn.x + ox, y1 = fn.y + oy;
const x2 = tn.x + ox, y2 = tn.y + oy;
const color = inTree ? '#1565c0' : (rCost < 0 ? '#c62828' : '#9aaebc');
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.lineWidth = inTree ? 2.8 : 1.6;
ctx.strokeStyle = color;
ctx.setLineDash(inTree ? [] : [5, 4]);
ctx.stroke();
ctx.setLineDash([]);
arrowHead(x1, y1, x2, y2, color);
// Label: cost (and flow for tree arcs, reduced cost for non-tree)
const mx = (x1 + x2)/2, my = (y1 + y2)/2;
const lx = mx + (-dy/len)*12, ly = my + (dx/len)*12; // push label sideways
ctx.font = '10.5px ui-monospace, monospace';
ctx.fillStyle = inTree ? '#0d47a1' : (rCost < 0 ? '#b71c1c' : '#546e7a');
const lbl = inTree ? `c${e.cost} f${flow[ei]}` : `c${e.cost} r${rCost}`;
ctx.textAlign = 'center';
ctx.fillText(lbl, lx, ly);
}
ctx.textAlign = 'left';
// Draw nodes
for (const n of NODES) {
ctx.beginPath();
ctx.arc(n.x, n.y, 19, 0, 2*Math.PI);
ctx.fillStyle = n.type==='supply' ? '#2e7d32' : n.type==='demand' ? '#c62828' : '#e8eef3';
ctx.fill();
ctx.strokeStyle = n.type==='supply' ? '#1b5e20' : n.type==='demand' ? '#7f0000' : '#8898a9';
ctx.lineWidth = 2;
ctx.stroke();
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = 'bold 13px system-ui';
ctx.fillStyle = n.type==='neutral' ? '#1d3557' : '#fff';
ctx.fillText(n.label, n.x, n.y - 2);
ctx.font = '10px system-ui';
ctx.fillStyle = n.type==='neutral' ? '#78909c' : '#cfd8dc';
ctx.fillText((n.b > 0 ? '+' : '') + n.b, n.x, n.y + 8);
}
ctx.textAlign = 'left';
ctx.textBaseline = 'alphabetic';
}
// โโ UI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const info = document.getElementById('info');
const pivotBtn = document.getElementById('pivot');
function setInfo(msg) { info.textContent = msg; }
pivotBtn.addEventListener('click', () => {
const result = pivot();
draw();
if (!result) {
setInfo(fmt('Optimal! All reduced costs โฅ 0. Total cost = %totalCost% after %pivotCount% pivot(s).', { totalCost: totalCost(), pivotCount: pivotCount }));
pivotBtn.disabled = true;
} else {
const degStr = result.delta === 0 ? ' (degenerate pivot, ฮ=0)' : '';
setInfo(fmt('Pivot %pivotCount%: arc %enterName% IN,, arc %leaveName% OUT (ฮ=%delta%, rc=%rc%)%degStr%. Cost = %totalCost%.', { pivotCount: pivotCount, enterName: result.enterName, leaveName: result.leaveName, delta: result.delta, rc: result.rc, degStr: degStr, totalCost: totalCost() }));
}
});
document.getElementById('reset').addEventListener('click', () => {
initState();
pivotBtn.disabled = false;
setInfo('Reduced costs shown on dashed non-tree edges. Red = negative (can improve).');
draw();
});
// โโ Boot & verification โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
initState();
draw();
// Verify algorithm offline: run to completion, check optimal cost = 12
(function verify() {
const savedTree = new Set(treeSet);
const savedFlow = [...flow];
const savedOptimal = optimal;
const savedPivotCount = pivotCount;
initState();
let steps = 0;
while (steps < 20) {
const r = pivot();
if (!r) break;
steps++;
}
const optCost = totalCost();
// Optimal: route all 3 units via SโBโCโT at cost 1+2+1=4/unit โ total 12
console.assert(optCost === 12, fmt('Optimal cost should be 12, got %optCost%', { optCost: optCost }));
console.assert(optimal === true, 'Should have reached optimal');
console.log('Verification: optimal cost =', optCost, '(expected 12) after', steps, 'pivots โ');
// Restore initial display state
treeSet = savedTree;
flow = savedFlow;
optimal = savedOptimal;
pivotCount = savedPivotCount;
draw();
})();
Comments
Loading comments...