Try it. You have jobs of different sizes and three servers. Click a job to move it to the next server. The bars show each server's load; the number you're minimizing is the busiest server's load (the makespan).
<p class="hint">Seven jobs of different sizes, three servers. Click a job to move it to the next server. The bars show each server's load — minimize the <b>busiest</b> one (the makespan).</p>
<div id="servers" class="servers"></div>
<div class="meter">Busiest server: <b id="make" class="make">0</b><span id="status" class="status"></span></div>
<div class="btns">
<button id="lpt" type="button">LPT (greedy)</button>
<button id="opt" type="button">Optimal</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 .8rem; line-height: 1.45; }
.servers { display: grid; grid-template-columns: repeat(3, 1fr); gap: .6rem; }
.srv { background: #f6f8fa; border: 1px solid #e6e9ee; border-radius: 10px; padding: .5rem; }
.srv h4 { margin: 0 0 .2rem; font: 700 12px system-ui; color: #1d3557; text-align: center; }
.load { text-align: center; font: 800 14px ui-monospace, monospace; color: #457b9d; margin-bottom: .4rem; }
.stack { display: flex; flex-direction: column-reverse; gap: 3px; min-height: 40px; }
.job { color: #fff; border-radius: 5px; text-align: center; font: 800 12px ui-monospace, monospace; cursor: pointer; padding: 2px 0; }
.meter { margin: .8rem 0 .6rem; font-size: 1.05rem; }
.make { font-family: ui-monospace, monospace; color: #c0392b; }
.status { margin-left: .6rem; font: 700 .9rem system-ui; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
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] ?? ''));
// Load balancing = multiprocessor scheduling (NP-hard). Minimize the makespan
// (busiest server). LPT (largest-first greedy) is a 4/3-approximation; Optimal
// brute-forces the minimum. Here LPT gives 12, the true optimum is 11.
const M = 3;
const SIZES = [7, 6, 5, 4, 4, 3, 3];
const COL = ['#2a9d8f', '#457b9d', '#6d597a', '#e76f51', '#b9770e', '#0a7d33', '#c0392b'];
let assign = SIZES.map((_, i) => i % M); // initial round-robin
function loads() { const l = new Array(M).fill(0); SIZES.forEach((s, i) => l[assign[i]] += s); return l; }
let OPT = Infinity;
(function () { const l = new Array(M).fill(0); (function rec(i) { if (i === SIZES.length) { OPT = Math.min(OPT, Math.max(...l)); return; } for (let s = 0; s < M; s++) { l[s] += SIZES[i]; if (l[s] < OPT) rec(i + 1); l[s] -= SIZES[i]; } })(0); })();
function render() {
const l = loads(), maxL = Math.max(...l, 1);
document.getElementById('servers').innerHTML = Array.from({ length: M }, (_, s) => {
const jobs = SIZES.map((sz, i) => ({ sz, i })).filter(j => assign[j.i] === s);
return fmt('<div class="srv"><h4>Server %s%%v%</h4><div class="load">%v2%</div><div class="stack" data-s="%s%">%map%</div></div>', { s: s, v: 1, v2: l[s], map: jobs.map(j => '<div class="job" data-i="' + j.i + '" style="background:' + COL[j.i] + ';height:' + (j.sz * 7 + 8) + 'px;line-height:' + (j.sz * 7 + 4) + 'px">' + j.sz + '</div>').join('') });
}).join('');
document.querySelectorAll('.job').forEach(el => el.addEventListener('click', () => { const i = +el.dataset.i; assign[i] = (assign[i] + 1) % M; render(); }));
const mk = Math.max(...l);
document.getElementById('make').textContent = mk;
const st = document.getElementById('status');
if (mk <= OPT) { st.style.color = '#0a7d33'; st.textContent = fmt('✓ optimal balance! (%OPT%)', { OPT: OPT }); }
else { st.style.color = '#b9770e'; st.textContent = fmt('optimum is %OPT% — %v% over', { OPT: OPT, v: mk - OPT }); }
}
function lpt() {
const order = SIZES.map((s, i) => i).sort((a, b) => SIZES[b] - SIZES[a]);
const l = new Array(M).fill(0); const a = SIZES.slice().fill(0);
for (const i of order) { let m = 0; for (let s = 1; s < M; s++) if (l[s] < l[m]) m = s; a[i] = m; l[m] += SIZES[i]; }
assign = a; render();
}
function optimal() {
const l = new Array(M).fill(0); let best = Infinity, bestA = null; const a = new Array(SIZES.length).fill(0);
(function rec(i) { if (i === SIZES.length) { const mk = Math.max(...l); if (mk < best) { best = mk; bestA = a.slice(); } return; } for (let s = 0; s < M; s++) { a[i] = s; l[s] += SIZES[i]; if (l[s] < best) rec(i + 1); l[s] -= SIZES[i]; } })(0);
assign = bestA; render();
}
document.getElementById('lpt').addEventListener('click', lpt);
document.getElementById('opt').addEventListener('click', optimal);
document.getElementById('reset').addEventListener('click', () => { assign = SIZES.map((_, i) => i % M); render(); });
render();
<p class="hint">Seven jobs of different sizes, three servers. Click a job to move it to the next server. The bars show each server's load — minimize the <b>busiest</b> one (the makespan).</p>
<div id="servers" class="servers"></div>
<div class="meter">Busiest server: <b id="make" class="make">0</b><span id="status" class="status"></span></div>
<div class="btns">
<button id="lpt" type="button">LPT (greedy)</button>
<button id="opt" type="button">Optimal</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 .8rem; line-height: 1.45; }
.servers { display: grid; grid-template-columns: repeat(3, 1fr); gap: .6rem; }
.srv { background: #f6f8fa; border: 1px solid #e6e9ee; border-radius: 10px; padding: .5rem; }
.srv h4 { margin: 0 0 .2rem; font: 700 12px system-ui; color: #1d3557; text-align: center; }
.load { text-align: center; font: 800 14px ui-monospace, monospace; color: #457b9d; margin-bottom: .4rem; }
.stack { display: flex; flex-direction: column-reverse; gap: 3px; min-height: 40px; }
.job { color: #fff; border-radius: 5px; text-align: center; font: 800 12px ui-monospace, monospace; cursor: pointer; padding: 2px 0; }
.meter { margin: .8rem 0 .6rem; font-size: 1.05rem; }
.make { font-family: ui-monospace, monospace; color: #c0392b; }
.status { margin-left: .6rem; font: 700 .9rem system-ui; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
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] ?? ''));
// Load balancing = multiprocessor scheduling (NP-hard). Minimize the makespan
// (busiest server). LPT (largest-first greedy) is a 4/3-approximation; Optimal
// brute-forces the minimum. Here LPT gives 12, the true optimum is 11.
const M = 3;
const SIZES = [7, 6, 5, 4, 4, 3, 3];
const COL = ['#2a9d8f', '#457b9d', '#6d597a', '#e76f51', '#b9770e', '#0a7d33', '#c0392b'];
let assign = SIZES.map((_, i) => i % M); // initial round-robin
function loads() { const l = new Array(M).fill(0); SIZES.forEach((s, i) => l[assign[i]] += s); return l; }
let OPT = Infinity;
(function () { const l = new Array(M).fill(0); (function rec(i) { if (i === SIZES.length) { OPT = Math.min(OPT, Math.max(...l)); return; } for (let s = 0; s < M; s++) { l[s] += SIZES[i]; if (l[s] < OPT) rec(i + 1); l[s] -= SIZES[i]; } })(0); })();
function render() {
const l = loads(), maxL = Math.max(...l, 1);
document.getElementById('servers').innerHTML = Array.from({ length: M }, (_, s) => {
const jobs = SIZES.map((sz, i) => ({ sz, i })).filter(j => assign[j.i] === s);
return fmt('<div class="srv"><h4>Server %s%%v%</h4><div class="load">%v2%</div><div class="stack" data-s="%s%">%map%</div></div>', { s: s, v: 1, v2: l[s], map: jobs.map(j => '<div class="job" data-i="' + j.i + '" style="background:' + COL[j.i] + ';height:' + (j.sz * 7 + 8) + 'px;line-height:' + (j.sz * 7 + 4) + 'px">' + j.sz + '</div>').join('') });
}).join('');
document.querySelectorAll('.job').forEach(el => el.addEventListener('click', () => { const i = +el.dataset.i; assign[i] = (assign[i] + 1) % M; render(); }));
const mk = Math.max(...l);
document.getElementById('make').textContent = mk;
const st = document.getElementById('status');
if (mk <= OPT) { st.style.color = '#0a7d33'; st.textContent = fmt('✓ optimal balance! (%OPT%)', { OPT: OPT }); }
else { st.style.color = '#b9770e'; st.textContent = fmt('optimum is %OPT% — %v% over', { OPT: OPT, v: mk - OPT }); }
}
function lpt() {
const order = SIZES.map((s, i) => i).sort((a, b) => SIZES[b] - SIZES[a]);
const l = new Array(M).fill(0); const a = SIZES.slice().fill(0);
for (const i of order) { let m = 0; for (let s = 1; s < M; s++) if (l[s] < l[m]) m = s; a[i] = m; l[m] += SIZES[i]; }
assign = a; render();
}
function optimal() {
const l = new Array(M).fill(0); let best = Infinity, bestA = null; const a = new Array(SIZES.length).fill(0);
(function rec(i) { if (i === SIZES.length) { const mk = Math.max(...l); if (mk < best) { best = mk; bestA = a.slice(); } return; } for (let s = 0; s < M; s++) { a[i] = s; l[s] += SIZES[i]; if (l[s] < best) rec(i + 1); l[s] -= SIZES[i]; } })(0);
assign = bestA; render();
}
document.getElementById('lpt').addEventListener('click', lpt);
document.getElementById('opt').addEventListener('click', optimal);
document.getElementById('reset').addEventListener('click', () => { assign = SIZES.map((_, i) => i % M); render(); });
render();
Try to even them out by hand, then press LPT (a smart greedy: place the biggest jobs first) and Optimal (the true best). Notice that even LPT — very good in practice — can miss the optimum here by a little. That small gap is the NP-hardness, and chasing it perfectly is the hard part.
Comments
Loading comments...