Below is a collection of sets, each showing the elements it contains. Click a set to add it to your packing — but you can only add a set if it shares no element with the ones you already picked. Try to grab as many as you can.
<p class="hint">Each card is a set of elements. Click a set to add it to your packing — but only if it shares <b>no element</b> with the sets you already picked. Grab as many disjoint sets as you can, then compare with greedy and the true optimum.</p>
<div id="sets" class="sets"></div>
<div class="status" id="status">Picked: 0 sets.</div>
<div class="btns">
<button id="greedy" type="button">Greedy pick</button>
<button id="optimum" type="button">Show optimum</button>
<button id="reset" type="button" class="ghost">Clear</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; }
.sets { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; margin: .4rem 0; }
.card { border: 2px solid #cdd9e3; background: #f3f7fa; border-radius: 10px; padding: .55rem .6rem; cursor: pointer; transition: all .12s; }
.card:hover { border-color: #1d3557; }
.card .name { font: 700 14px system-ui, sans-serif; color: #1d3557; margin-bottom: .35rem; }
.card .els { display: flex; flex-wrap: wrap; gap: 4px; }
.chip { font: 600 12px ui-monospace, monospace; background: #dde7ef; color: #1d3557; border-radius: 6px; padding: 2px 7px; }
.card.picked { background: #d6f0dd; border-color: #0a7d33; }
.card.picked .name { color: #0a7d33; }
.card.blocked { opacity: .4; cursor: not-allowed; }
.card.blocked .chip { background: #f0d6d6; color: #c92f3c; }
.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; }
button { font: 600 14px system-ui, sans-serif; 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] ?? ''));
// SET PACKING: choose the most sets so that no two share an element.
// Checking a chosen family (pairwise-disjoint?) is instant; finding the LARGEST
// such family is NP-hard (Karp, 1972) — equivalent to Maximum Independent Set. This instance is built so a natural greedy (smallest-set-first) is NOT optimal.
const SETS = [
{ name: 'A', els: [1, 2, 3] }, // big, in the optimum
{ name: 'B', els: [4, 5, 6] }, // big, in the optimum
{ name: 'C', els: [7, 8] }, // small, in the optimum
{ name: 'X', els: [3, 4] }, // SMALL trap: shares 3 with A and 4 with B
{ name: 'Y', els: [9, 10] } // small, in the optimum
];
const overlap = (x, y) => x.els.some(e => y.els.includes(e));
let picked = new Set(); // indices the player chose
let highlight = null; // 'greedy' | 'optimum' | null
function disjointWithPicked(i, chosen) {
for (const j of chosen) if (j !== i && overlap(SETS[i], SETS[j])) return false;
return true;
}
function render() {
const wrap = document.getElementById('sets'); wrap.innerHTML = '';
SETS.forEach((s, i) => {
const d = document.createElement('div');
const isPicked = picked.has(i);
const blocked = !isPicked && !disjointWithPicked(i, picked);
d.className = 'card' + (isPicked ? ' picked' : '') + (blocked ? ' blocked' : '');
d.innerHTML = fmt('<div class="name">Set %name%</div><div class="els">%map%</div>', { name: s.name, map: s.els.map(e => '<span class="chip">' + e + '</span>').join('') });
d.addEventListener('click', () => {
if (picked.has(i)) { picked.delete(i); }
else { if (!disjointWithPicked(i, picked)) { flash(fmt('Set %name% shares an element — cannot add.', { name: s.name }), 'bad'); return; } picked.add(i); }
highlight = null; render(); report();
});
wrap.appendChild(d);
});
}
function report() {
setStatus(fmt('Picked: %size% disjoint set%v%.', { size: picked.size, v: picked.size === 1 ? '' : 's' }), picked.size >= 3 ? 'ok' : '');
}
function setStatus(t, cls) { const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + cls; }
function flash(t, cls) { setStatus(t, cls); }
// Greedy: repeatedly take the smallest available set disjoint from what's chosen.
function greedy() {
const chosen = new Set();
const order = SETS.map((s, i) => i).sort((a, b) => SETS[a].els.length - SETS[b].els.length || a - b);
for (const i of order) if (disjointWithPicked(i, chosen)) chosen.add(i);
picked = chosen; highlight = 'greedy'; render();
setStatus(fmt('Greedy (smallest-first) packed %size% sets: %sort%.', { size: chosen.size, sort: [...chosen].map(i => SETS[i].name).sort().join(', ') }), 'bad');
}
// Optimum: brute-force every subset, keep the largest pairwise-disjoint one.
function optimum() {
let best = [];
for (let mask = 0; mask < (1 << SETS.length); mask++) {
const sub = [];
for (let i = 0; i < SETS.length; i++) if (mask & (1 << i)) sub.push(i);
let ok = true;
for (let a = 0; a < sub.length && ok; a++)
for (let b = a + 1; b < sub.length; b++)
if (overlap(SETS[sub[a]], SETS[sub[b]])) { ok = false; break; }
if (ok && sub.length > best.length) best = sub;
}
picked = new Set(best); highlight = 'optimum'; render();
setStatus(fmt('Optimum packs %length% sets: %sort%. Greedy gets only 3.', { length: best.length, sort: best.map(i => SETS[i].name).sort().join(', ') }), 'ok');
console.log('Optimum:', best.map(i => SETS[i].name).join(','), '(size', best.length + ')');
}
document.getElementById('greedy').addEventListener('click', greedy);
document.getElementById('optimum').addEventListener('click', optimum);
document.getElementById('reset').addEventListener('click', () => { picked.clear(); highlight = null; render(); report(); });
render(); report();
<p class="hint">Each card is a set of elements. Click a set to add it to your packing — but only if it shares <b>no element</b> with the sets you already picked. Grab as many disjoint sets as you can, then compare with greedy and the true optimum.</p>
<div id="sets" class="sets"></div>
<div class="status" id="status">Picked: 0 sets.</div>
<div class="btns">
<button id="greedy" type="button">Greedy pick</button>
<button id="optimum" type="button">Show optimum</button>
<button id="reset" type="button" class="ghost">Clear</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; }
.sets { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; margin: .4rem 0; }
.card { border: 2px solid #cdd9e3; background: #f3f7fa; border-radius: 10px; padding: .55rem .6rem; cursor: pointer; transition: all .12s; }
.card:hover { border-color: #1d3557; }
.card .name { font: 700 14px system-ui, sans-serif; color: #1d3557; margin-bottom: .35rem; }
.card .els { display: flex; flex-wrap: wrap; gap: 4px; }
.chip { font: 600 12px ui-monospace, monospace; background: #dde7ef; color: #1d3557; border-radius: 6px; padding: 2px 7px; }
.card.picked { background: #d6f0dd; border-color: #0a7d33; }
.card.picked .name { color: #0a7d33; }
.card.blocked { opacity: .4; cursor: not-allowed; }
.card.blocked .chip { background: #f0d6d6; color: #c92f3c; }
.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; }
button { font: 600 14px system-ui, sans-serif; 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] ?? ''));
// SET PACKING: choose the most sets so that no two share an element.
// Checking a chosen family (pairwise-disjoint?) is instant; finding the LARGEST
// such family is NP-hard (Karp, 1972) — equivalent to Maximum Independent Set. This instance is built so a natural greedy (smallest-set-first) is NOT optimal.
const SETS = [
{ name: 'A', els: [1, 2, 3] }, // big, in the optimum
{ name: 'B', els: [4, 5, 6] }, // big, in the optimum
{ name: 'C', els: [7, 8] }, // small, in the optimum
{ name: 'X', els: [3, 4] }, // SMALL trap: shares 3 with A and 4 with B
{ name: 'Y', els: [9, 10] } // small, in the optimum
];
const overlap = (x, y) => x.els.some(e => y.els.includes(e));
let picked = new Set(); // indices the player chose
let highlight = null; // 'greedy' | 'optimum' | null
function disjointWithPicked(i, chosen) {
for (const j of chosen) if (j !== i && overlap(SETS[i], SETS[j])) return false;
return true;
}
function render() {
const wrap = document.getElementById('sets'); wrap.innerHTML = '';
SETS.forEach((s, i) => {
const d = document.createElement('div');
const isPicked = picked.has(i);
const blocked = !isPicked && !disjointWithPicked(i, picked);
d.className = 'card' + (isPicked ? ' picked' : '') + (blocked ? ' blocked' : '');
d.innerHTML = fmt('<div class="name">Set %name%</div><div class="els">%map%</div>', { name: s.name, map: s.els.map(e => '<span class="chip">' + e + '</span>').join('') });
d.addEventListener('click', () => {
if (picked.has(i)) { picked.delete(i); }
else { if (!disjointWithPicked(i, picked)) { flash(fmt('Set %name% shares an element — cannot add.', { name: s.name }), 'bad'); return; } picked.add(i); }
highlight = null; render(); report();
});
wrap.appendChild(d);
});
}
function report() {
setStatus(fmt('Picked: %size% disjoint set%v%.', { size: picked.size, v: picked.size === 1 ? '' : 's' }), picked.size >= 3 ? 'ok' : '');
}
function setStatus(t, cls) { const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + cls; }
function flash(t, cls) { setStatus(t, cls); }
// Greedy: repeatedly take the smallest available set disjoint from what's chosen.
function greedy() {
const chosen = new Set();
const order = SETS.map((s, i) => i).sort((a, b) => SETS[a].els.length - SETS[b].els.length || a - b);
for (const i of order) if (disjointWithPicked(i, chosen)) chosen.add(i);
picked = chosen; highlight = 'greedy'; render();
setStatus(fmt('Greedy (smallest-first) packed %size% sets: %sort%.', { size: chosen.size, sort: [...chosen].map(i => SETS[i].name).sort().join(', ') }), 'bad');
}
// Optimum: brute-force every subset, keep the largest pairwise-disjoint one.
function optimum() {
let best = [];
for (let mask = 0; mask < (1 << SETS.length); mask++) {
const sub = [];
for (let i = 0; i < SETS.length; i++) if (mask & (1 << i)) sub.push(i);
let ok = true;
for (let a = 0; a < sub.length && ok; a++)
for (let b = a + 1; b < sub.length; b++)
if (overlap(SETS[sub[a]], SETS[sub[b]])) { ok = false; break; }
if (ok && sub.length > best.length) best = sub;
}
picked = new Set(best); highlight = 'optimum'; render();
setStatus(fmt('Optimum packs %length% sets: %sort%. Greedy gets only 3.', { length: best.length, sort: best.map(i => SETS[i].name).sort().join(', ') }), 'ok');
console.log('Optimum:', best.map(i => SETS[i].name).join(','), '(size', best.length + ')');
}
document.getElementById('greedy').addEventListener('click', greedy);
document.getElementById('optimum').addEventListener('click', optimum);
document.getElementById('reset').addEventListener('click', () => { picked.clear(); highlight = null; render(); report(); });
render(); report();
Comments
Loading comments...