Below is the classic Braess network. 4000 drivers travel from Start to End. Two roads have a fixed 45-minute cost; the other two get more congested the more cars use them (their time is cars ÷ 100 minutes). A dashed shortcut in the middle is instant — and you can open or close it.
<p class="hint">4000 drivers go from <b>Start</b> to <b>End</b>. Two roads cost a flat 45 min; two get slower with traffic (cars÷100 min). The dashed <b>shortcut</b> is instant. Toggle it and watch every driver’s time — selfish routing finds the worst spot.</p>
<div class="net">
<svg viewBox="0 0 320 200" id="svg">
<line class="edge cong" x1="40" y1="100" x2="160" y2="40" />
<line class="edge fix" x1="160" y1="40" x2="280" y2="100" />
<line class="edge fix" x1="40" y1="100" x2="160" y2="160" />
<line class="edge cong" x1="160" y1="160" x2="280" y2="100" />
<line class="edge cut" id="cut" x1="160" y1="40" x2="160" y2="160" />
<text class="lbl" x="86" y="58">cars/100</text>
<text class="lbl" x="214" y="58">45</text>
<text class="lbl" x="86" y="150">45</text>
<text class="lbl" x="214" y="150">cars/100</text>
<text class="lbl cutlbl" id="cutlbl" x="168" y="104">0</text>
<circle class="node" cx="40" cy="100" r="13" /><text class="nlbl" x="40" y="104">S</text>
<circle class="node" cx="160" cy="40" r="13" /><text class="nlbl" x="160" y="44">A</text>
<circle class="node" cx="160" cy="160" r="13" /><text class="nlbl" x="160" y="164">B</text>
<circle class="node" cx="280" cy="100" r="13" /><text class="nlbl" x="280" y="104">E</text>
</svg>
</div>
<div class="status" id="status"></div>
<div class="btns">
<button id="toggle" type="button">Open the shortcut</button>
<button id="solve" type="button" class="ghost">Let drivers settle (equilibrium)</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 .6rem; line-height: 1.45; }
.net { display: flex; justify-content: center; }
svg { width: 100%; max-width: 360px; height: auto; }
.edge { stroke: #6b7785; stroke-width: 5; stroke-linecap: round; }
.edge.cong { stroke: #e8833a; }
.edge.fix { stroke: #3b6fb0; }
.edge.cut { stroke: #2aa15a; stroke-width: 4; stroke-dasharray: 6 6; opacity: .2; }
.edge.cut.on { opacity: 1; }
.node { fill: #1d3557; }
.nlbl { fill: #fff; font: 700 13px system-ui; text-anchor: middle; }
.lbl { fill: #555; font: 600 11px ui-monospace, monospace; text-anchor: middle; }
.cutlbl { fill: #2aa15a; }
.status { font-size: .98rem; font-weight: 600; margin: .6rem 0; min-height: 3em; line-height: 1.4; }
.big { font-size: 1.5rem; }
.ok { color: #0a7d33; } .bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Classic Braess network. T drivers from S to E.
// S->A : congestible, time = cars/100
// A->E : fixed 45
// S->B : fixed 45
// B->E : congestible, time = cars/100
// A->B : shortcut, instant (only when open)
// Routes: top = S-A-E, bottom = S-B-E, cross = S-A-B-E (uses shortcut)
const T = 4000;
let shortcutOpen = false;
// Travel time for a split (a, b, c) = drivers on top, bottom, cross. a+b+c=T.
// S->A carries a+c ; B->E carries b+c ; A->E carries a ; S->B carries b.
function times(a, b, c) {
const sa = (a + c) / 100; // congestible
const be = (b + c) / 100; // congestible
const top = sa + 45; // S-A-E
const bottom = 45 + be; // S-B-E
const cross = sa + 0 + be; // S-A-B-E (shortcut = 0)
return { top, bottom, cross, sa, be };
}
// Selfish (Nash) equilibrium.
function equilibrium() {
if (!shortcutOpen) {
// No cross route. Split so top == bottom. By symmetry a=b=T/2, c=0.
const a = T / 2, b = T / 2;
const t = times(a, b, 0);
return { a, b, c: 0, time: t.top };
}
// Shortcut open: everyone is drawn to the cross route until all on it.
// At a=b=0, c=T: cross = T/100 + T/100 = 80; top = T/100+45 = 85 > 80; bottom = 45 + T/100 = 85 > 80. So nobody can improve -> all cross.
const t = times(0, 0, T);
return { a: 0, b: 0, c: T, time: t.cross };
}
const $ = id => document.getElementById(id);
function setStatus(html) { $('status').innerHTML = html; }
function render(eq) {
$('cut').classList.toggle('on', shortcutOpen);
const cls = eq.time > 70 ? 'bad' : 'ok';
const split = shortcutOpen
? fmt('All <b>%T%</b> drivers funnel onto the shortcut route (S→A→B→E).', { T: T })
: fmt('Drivers split <b><b>%a%</b></b> on top and <b><b>%b%</b></b> on the bottom.', { a: eq.a, b: eq.b });
setStatus(fmt('%split%<br>Everyone’s commute: <span class="big %cls%">%time% min</span>%v%', { split: split, cls: cls, time: eq.time, v: eq.time > 70 ? ' — <b>slower!</b>' : '' }));
}
function update() {
$('toggle').textContent = shortcutOpen ? 'Close the shortcut' : 'Open the shortcut';
render(equilibrium());
}
$('toggle').addEventListener('click', () => { shortcutOpen = !shortcutOpen; update(); });
$('solve').addEventListener('click', () => {
const eq = equilibrium();
console.log('shortcut', shortcutOpen ? 'OPEN' : 'closed',
'| split', eq.a, eq.b, eq.c, '| time', eq.time, 'min');
render(eq);
});
update();
<p class="hint">4000 drivers go from <b>Start</b> to <b>End</b>. Two roads cost a flat 45 min; two get slower with traffic (cars÷100 min). The dashed <b>shortcut</b> is instant. Toggle it and watch every driver’s time — selfish routing finds the worst spot.</p>
<div class="net">
<svg viewBox="0 0 320 200" id="svg">
<line class="edge cong" x1="40" y1="100" x2="160" y2="40" />
<line class="edge fix" x1="160" y1="40" x2="280" y2="100" />
<line class="edge fix" x1="40" y1="100" x2="160" y2="160" />
<line class="edge cong" x1="160" y1="160" x2="280" y2="100" />
<line class="edge cut" id="cut" x1="160" y1="40" x2="160" y2="160" />
<text class="lbl" x="86" y="58">cars/100</text>
<text class="lbl" x="214" y="58">45</text>
<text class="lbl" x="86" y="150">45</text>
<text class="lbl" x="214" y="150">cars/100</text>
<text class="lbl cutlbl" id="cutlbl" x="168" y="104">0</text>
<circle class="node" cx="40" cy="100" r="13" /><text class="nlbl" x="40" y="104">S</text>
<circle class="node" cx="160" cy="40" r="13" /><text class="nlbl" x="160" y="44">A</text>
<circle class="node" cx="160" cy="160" r="13" /><text class="nlbl" x="160" y="164">B</text>
<circle class="node" cx="280" cy="100" r="13" /><text class="nlbl" x="280" y="104">E</text>
</svg>
</div>
<div class="status" id="status"></div>
<div class="btns">
<button id="toggle" type="button">Open the shortcut</button>
<button id="solve" type="button" class="ghost">Let drivers settle (equilibrium)</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 .6rem; line-height: 1.45; }
.net { display: flex; justify-content: center; }
svg { width: 100%; max-width: 360px; height: auto; }
.edge { stroke: #6b7785; stroke-width: 5; stroke-linecap: round; }
.edge.cong { stroke: #e8833a; }
.edge.fix { stroke: #3b6fb0; }
.edge.cut { stroke: #2aa15a; stroke-width: 4; stroke-dasharray: 6 6; opacity: .2; }
.edge.cut.on { opacity: 1; }
.node { fill: #1d3557; }
.nlbl { fill: #fff; font: 700 13px system-ui; text-anchor: middle; }
.lbl { fill: #555; font: 600 11px ui-monospace, monospace; text-anchor: middle; }
.cutlbl { fill: #2aa15a; }
.status { font-size: .98rem; font-weight: 600; margin: .6rem 0; min-height: 3em; line-height: 1.4; }
.big { font-size: 1.5rem; }
.ok { color: #0a7d33; } .bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Classic Braess network. T drivers from S to E.
// S->A : congestible, time = cars/100
// A->E : fixed 45
// S->B : fixed 45
// B->E : congestible, time = cars/100
// A->B : shortcut, instant (only when open)
// Routes: top = S-A-E, bottom = S-B-E, cross = S-A-B-E (uses shortcut)
const T = 4000;
let shortcutOpen = false;
// Travel time for a split (a, b, c) = drivers on top, bottom, cross. a+b+c=T.
// S->A carries a+c ; B->E carries b+c ; A->E carries a ; S->B carries b.
function times(a, b, c) {
const sa = (a + c) / 100; // congestible
const be = (b + c) / 100; // congestible
const top = sa + 45; // S-A-E
const bottom = 45 + be; // S-B-E
const cross = sa + 0 + be; // S-A-B-E (shortcut = 0)
return { top, bottom, cross, sa, be };
}
// Selfish (Nash) equilibrium.
function equilibrium() {
if (!shortcutOpen) {
// No cross route. Split so top == bottom. By symmetry a=b=T/2, c=0.
const a = T / 2, b = T / 2;
const t = times(a, b, 0);
return { a, b, c: 0, time: t.top };
}
// Shortcut open: everyone is drawn to the cross route until all on it.
// At a=b=0, c=T: cross = T/100 + T/100 = 80; top = T/100+45 = 85 > 80; bottom = 45 + T/100 = 85 > 80. So nobody can improve -> all cross.
const t = times(0, 0, T);
return { a: 0, b: 0, c: T, time: t.cross };
}
const $ = id => document.getElementById(id);
function setStatus(html) { $('status').innerHTML = html; }
function render(eq) {
$('cut').classList.toggle('on', shortcutOpen);
const cls = eq.time > 70 ? 'bad' : 'ok';
const split = shortcutOpen
? fmt('All <b>%T%</b> drivers funnel onto the shortcut route (S→A→B→E).', { T: T })
: fmt('Drivers split <b><b>%a%</b></b> on top and <b><b>%b%</b></b> on the bottom.', { a: eq.a, b: eq.b });
setStatus(fmt('%split%<br>Everyone’s commute: <span class="big %cls%">%time% min</span>%v%', { split: split, cls: cls, time: eq.time, v: eq.time > 70 ? ' — <b>slower!</b>' : '' }));
}
function update() {
$('toggle').textContent = shortcutOpen ? 'Close the shortcut' : 'Open the shortcut';
render(equilibrium());
}
$('toggle').addEventListener('click', () => { shortcutOpen = !shortcutOpen; update(); });
$('solve').addEventListener('click', () => {
const eq = equilibrium();
console.log('shortcut', shortcutOpen ? 'OPEN' : 'closed',
'| split', eq.a, eq.b, eq.c, '| time', eq.time, 'min');
render(eq);
});
update();
With the shortcut closed, drivers split evenly and everyone needs 65 minutes. Open the shortcut and each driver, acting selfishly, is tempted onto it — until everyone funnels through the two congestible roads and the trip jumps to 80 minutes. The road that was supposed to help made the whole network 23% slower, and no driver can fix it alone. Close it again and the commute drops right back to 65.
Comments
Loading comments...