Try it. The dots are towns. Click two of them to build hospitals there; every town is then served by its nearest hospital. The demo highlights the worst-served town and the distance it must travel — the number you're trying to minimize.
<p class="hint">The dots are towns. Click <b>two</b> to build hospitals there — each town is served by its nearest hospital. The dashed line marks the <span class="r">worst-served</span> town; shrink that distance, then Optimize.</p>
<svg id="map" viewBox="0 0 320 230" class="map"></svg>
<div class="meter">
<div>Hospitals: <b id="k">0</b> / 2</div>
<div>Worst distance: <b id="worst" class="r">—</b></div>
<div id="msg" class="msg"></div>
</div>
<div class="btns">
<button id="opt" type="button">Optimize</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; }
.hint .r { color: #c0392b; font-weight: 700; }
.map { width: 100%; max-width: 460px; background: #f4f7f9; border: 1px solid #e2e6eb; border-radius: 10px; display: block; }
.town { fill: #8aa0b3; cursor: pointer; transition: fill .12s; }
.town:hover { fill: #457b9d; }
.fac { fill: #2a9d8f; stroke: #fff; stroke-width: 2; cursor: pointer; }
.worst { fill: #c0392b; }
.lbl { font: 600 9px ui-monospace, monospace; fill: #6a7b88; pointer-events: none; }
.link { stroke: #c0392b; stroke-width: 2; stroke-dasharray: 4 3; }
.meter { display: flex; gap: 1.3rem; align-items: center; font-size: 1rem; margin: .8rem 0 .6rem; flex-wrap: wrap; }
.meter .r { color: #c0392b; }
.msg { font-weight: 700; color: #0a7d33; }
.btns { display: flex; gap: .5rem; }
button { font: 600 14px system-ui, sans-serif; padding: .5rem 1rem; border: 1px solid #457b9d; background: #457b9d; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #457b9d; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Vertex k-center: choose k town sites for facilities to minimize the largest distance from any town to its nearest facility. NP-hard; the farthest-first greedy is a 2-approximation. Here we brute-force k=2.
const K = 2;
// towns: a left cluster plus two far-apart outliers -> placement matters
const TOWNS = [
{ x: 50, y: 60 }, { x: 70, y: 90 }, { x: 60, y: 120 }, { x: 90, y: 70 },
{ x: 80, y: 140 }, { x: 250, y: 50 }, { x: 270, y: 110 }, { x: 240, y: 170 }, { x: 150, y: 190 }
];
const N = TOWNS.length;
let fac = []; // indices of chosen facility towns
function dist(a, b) { return Math.hypot(TOWNS[a].x - TOWNS[b].x, TOWNS[a].y - TOWNS[b].y); }
// returns {worst: distance, town: index, nearest: facilityIndex}
function evaluate(facs) {
if (!facs.length) return { worst: Infinity, town: -1, nearest: -1 };
let worst = -1, wt = -1, wn = -1;
for (let t = 0; t < N; t++) {
let best = Infinity, bf = -1;
for (const f of facs) { const d = dist(t, f); if (d < best) { best = d; bf = f; } }
if (best > worst) { worst = best; wt = t; wn = bf; }
}
return { worst, town: wt, nearest: wn };
}
function toggle(i) {
const k = fac.indexOf(i);
if (k >= 0) fac.splice(k, 1);
else { if (fac.length >= K) fac.shift(); fac.push(i); }
render();
}
function render() {
const ev = evaluate(fac);
const svg = ['<svg/>'];
let s = '';
// worst link
if (fac.length && ev.town >= 0) {
s += '<line class="link" x1="' + TOWNS[ev.town].x + '" y1="' + TOWNS[ev.town].y +
'" x2="' + TOWNS[ev.nearest].x + '" y2="' + TOWNS[ev.nearest].y + '" />';
}
TOWNS.forEach((t, i) => {
const isFac = fac.includes(i);
if (isFac) s += '<rect class="fac" x="' + (t.x - 7) + '" y="' + (t.y - 7) + '" width="14" height="14" rx="3" data-i="' + i + '"/>';
else { const cls = (i === ev.town && fac.length) ? 'town worst' : 'town'; s += '<circle class="' + cls + '" cx="' + t.x + '" cy="' + t.y + '" r="7" data-i="' + i + '"/>'; }
});
document.getElementById('map').innerHTML = s;
document.getElementById('map').querySelectorAll('[data-i]').forEach(el =>
el.addEventListener('click', () => toggle(+el.dataset.i)));
document.getElementById('k').textContent = fac.length;
document.getElementById('worst').textContent = fac.length ? Math.round(ev.worst) : '—';
const msg = document.getElementById('msg');
if (fac.length < K) { msg.style.color = '#777'; msg.textContent = fmt('Place %v% more.', { v: K - fac.length }); }
else if (Math.round(ev.worst) <= OPT + 1) { msg.style.color = '#0a7d33'; msg.textContent = fmt('✓ Optimal placement! (worst ≈ %OPT%)', { OPT: OPT }); }
else { msg.style.color = '#b9770e'; msg.textContent = fmt('Optimal worst distance is %OPT% — keep trying.', { OPT: OPT }); }
}
function optimize() {
let best = null, bestW = Infinity;
for (let i = 0; i < N; i++) for (let j = i + 1; j < N; j++) {
const w = evaluate([i, j]).worst; if (w < bestW) { bestW = w; best = [i, j]; }
}
fac = best; render();
}
// precompute the optimum worst-distance for the success message
let OPT = Infinity;
for (let i = 0; i < N; i++) for (let j = i + 1; j < N; j++) OPT = Math.min(OPT, evaluate([i, j]).worst);
OPT = Math.round(OPT);
document.getElementById('opt').addEventListener('click', optimize);
document.getElementById('reset').addEventListener('click', () => { fac = []; render(); });
render();
<p class="hint">The dots are towns. Click <b>two</b> to build hospitals there — each town is served by its nearest hospital. The dashed line marks the <span class="r">worst-served</span> town; shrink that distance, then Optimize.</p>
<svg id="map" viewBox="0 0 320 230" class="map"></svg>
<div class="meter">
<div>Hospitals: <b id="k">0</b> / 2</div>
<div>Worst distance: <b id="worst" class="r">—</b></div>
<div id="msg" class="msg"></div>
</div>
<div class="btns">
<button id="opt" type="button">Optimize</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; }
.hint .r { color: #c0392b; font-weight: 700; }
.map { width: 100%; max-width: 460px; background: #f4f7f9; border: 1px solid #e2e6eb; border-radius: 10px; display: block; }
.town { fill: #8aa0b3; cursor: pointer; transition: fill .12s; }
.town:hover { fill: #457b9d; }
.fac { fill: #2a9d8f; stroke: #fff; stroke-width: 2; cursor: pointer; }
.worst { fill: #c0392b; }
.lbl { font: 600 9px ui-monospace, monospace; fill: #6a7b88; pointer-events: none; }
.link { stroke: #c0392b; stroke-width: 2; stroke-dasharray: 4 3; }
.meter { display: flex; gap: 1.3rem; align-items: center; font-size: 1rem; margin: .8rem 0 .6rem; flex-wrap: wrap; }
.meter .r { color: #c0392b; }
.msg { font-weight: 700; color: #0a7d33; }
.btns { display: flex; gap: .5rem; }
button { font: 600 14px system-ui, sans-serif; padding: .5rem 1rem; border: 1px solid #457b9d; background: #457b9d; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #457b9d; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Vertex k-center: choose k town sites for facilities to minimize the largest distance from any town to its nearest facility. NP-hard; the farthest-first greedy is a 2-approximation. Here we brute-force k=2.
const K = 2;
// towns: a left cluster plus two far-apart outliers -> placement matters
const TOWNS = [
{ x: 50, y: 60 }, { x: 70, y: 90 }, { x: 60, y: 120 }, { x: 90, y: 70 },
{ x: 80, y: 140 }, { x: 250, y: 50 }, { x: 270, y: 110 }, { x: 240, y: 170 }, { x: 150, y: 190 }
];
const N = TOWNS.length;
let fac = []; // indices of chosen facility towns
function dist(a, b) { return Math.hypot(TOWNS[a].x - TOWNS[b].x, TOWNS[a].y - TOWNS[b].y); }
// returns {worst: distance, town: index, nearest: facilityIndex}
function evaluate(facs) {
if (!facs.length) return { worst: Infinity, town: -1, nearest: -1 };
let worst = -1, wt = -1, wn = -1;
for (let t = 0; t < N; t++) {
let best = Infinity, bf = -1;
for (const f of facs) { const d = dist(t, f); if (d < best) { best = d; bf = f; } }
if (best > worst) { worst = best; wt = t; wn = bf; }
}
return { worst, town: wt, nearest: wn };
}
function toggle(i) {
const k = fac.indexOf(i);
if (k >= 0) fac.splice(k, 1);
else { if (fac.length >= K) fac.shift(); fac.push(i); }
render();
}
function render() {
const ev = evaluate(fac);
const svg = ['<svg/>'];
let s = '';
// worst link
if (fac.length && ev.town >= 0) {
s += '<line class="link" x1="' + TOWNS[ev.town].x + '" y1="' + TOWNS[ev.town].y +
'" x2="' + TOWNS[ev.nearest].x + '" y2="' + TOWNS[ev.nearest].y + '" />';
}
TOWNS.forEach((t, i) => {
const isFac = fac.includes(i);
if (isFac) s += '<rect class="fac" x="' + (t.x - 7) + '" y="' + (t.y - 7) + '" width="14" height="14" rx="3" data-i="' + i + '"/>';
else { const cls = (i === ev.town && fac.length) ? 'town worst' : 'town'; s += '<circle class="' + cls + '" cx="' + t.x + '" cy="' + t.y + '" r="7" data-i="' + i + '"/>'; }
});
document.getElementById('map').innerHTML = s;
document.getElementById('map').querySelectorAll('[data-i]').forEach(el =>
el.addEventListener('click', () => toggle(+el.dataset.i)));
document.getElementById('k').textContent = fac.length;
document.getElementById('worst').textContent = fac.length ? Math.round(ev.worst) : '—';
const msg = document.getElementById('msg');
if (fac.length < K) { msg.style.color = '#777'; msg.textContent = fmt('Place %v% more.', { v: K - fac.length }); }
else if (Math.round(ev.worst) <= OPT + 1) { msg.style.color = '#0a7d33'; msg.textContent = fmt('✓ Optimal placement! (worst ≈ %OPT%)', { OPT: OPT }); }
else { msg.style.color = '#b9770e'; msg.textContent = fmt('Optimal worst distance is %OPT% — keep trying.', { OPT: OPT }); }
}
function optimize() {
let best = null, bestW = Infinity;
for (let i = 0; i < N; i++) for (let j = i + 1; j < N; j++) {
const w = evaluate([i, j]).worst; if (w < bestW) { bestW = w; best = [i, j]; }
}
fac = best; render();
}
// precompute the optimum worst-distance for the success message
let OPT = Infinity;
for (let i = 0; i < N; i++) for (let j = i + 1; j < N; j++) OPT = Math.min(OPT, evaluate([i, j]).worst);
OPT = Math.round(OPT);
document.getElementById('opt').addEventListener('click', optimize);
document.getElementById('reset').addEventListener('click', () => { fac = []; render(); });
render();
Move your two hospitals around to shrink that worst distance, then hit Optimize for the best possible pair. Notice how the obvious "cover the cluster" choice can leave a lone town stranded — minimizing the maximum distance is a different, trickier goal than serving the crowd.
Comments
Loading comments...