Try it. Click nodes to select a group. The checker tells you live whether your selection is a clique (everyone connected), an independent set (no one connected), or neither.
<p class="hint">Click people to select a group. A <b>clique</b> = everyone knows everyone (all connected). An <b>independent set</b> = total strangers (none connected). The checker tells you live which one you've got.</p>
<svg id="g" viewBox="0 0 320 230" class="g"></svg>
<div id="status" class="status"></div>
<div class="btns">
<button id="mc" type="button">Max clique</button>
<button id="mi" type="button">Max independent set</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 .7rem; line-height: 1.45; }
.g { width: 100%; max-width: 460px; background: #f4f7f9; border: 1px solid #e2e6eb; border-radius: 10px; display: block; }
.edge { stroke: #c2ccd6; stroke-width: 2.5; }
.node { fill: #8aa0b3; cursor: pointer; transition: fill .1s; }
.node:hover { fill: #6f8699; }
.node.sel { fill: #457b9d; }
.node.clq { fill: #2a9d8f; }
.node.ind { fill: #b5651d; }
.nlbl { font: 700 12px ui-monospace, monospace; fill: #fff; text-anchor: middle; dominant-baseline: central; pointer-events: none; }
.status { font-weight: 800; min-height: 1.4em; margin: .8rem 0 .5rem; font-size: 1rem; }
.status.clq { color: #0a7d33; } .status.ind { color: #b5651d; } .status.no { color: #777; }
.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] ?? ''));
// Clique = all pairs connected; independent set = no pair connected.
// Both are NP-hard (and hard to approximate). Here we brute-force the maxima.
const N = 7;
const POS = [[110, 60], [210, 60], [210, 150], [110, 150], [40, 30], [285, 35], [285, 180]];
const E = [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3], [0, 4], [1, 5], [2, 6], [4, 5]];
const adj = Array.from({ length: N }, () => new Set());
E.forEach(([u, v]) => { adj[u].add(v); adj[v].add(u); });
let sel = new Set(), mode = '';
function classify(arr) {
let clq = true, ind = true;
for (let a = 0; a < arr.length; a++) for (let b = a + 1; b < arr.length; b++) {
if (adj[arr[a]].has(arr[b])) ind = false; else clq = false;
}
return { clq: clq && arr.length > 1, ind: ind && arr.length > 1 };
}
function render() {
const arr = [...sel];
const c = classify(arr);
let s = '';
E.forEach(([u, v]) => { s += '<line class="edge" x1="' + POS[u][0] + '" y1="' + POS[u][1] + '" x2="' + POS[v][0] + '" y2="' + POS[v][1] + '"/>'; });
for (let i = 0; i < N; i++) {
let cls = 'node';
if (sel.has(i)) cls += mode === 'clq' ? ' clq' : (mode === 'ind' ? ' ind' : ' sel');
s += '<circle class="' + cls + '" cx="' + POS[i][0] + '" cy="' + POS[i][1] + '" r="16" data-i="' + i + '"/>';
s += '<text class="nlbl" x="' + POS[i][0] + '" y="' + POS[i][1] + '">' + i + '</text>';
}
const svg = document.getElementById('g');
svg.innerHTML = s;
svg.querySelectorAll('.node').forEach(n => n.addEventListener('click', () => { mode = ''; const i = +n.dataset.i; sel.has(i) ? sel.delete(i) : sel.add(i); render(); }));
const st = document.getElementById('status');
if (arr.length < 2) { st.className = 'status no'; st.textContent = 'Select at least two people.'; }
else if (c.clq) { st.className = 'status clq'; st.textContent = fmt('✓ Clique of %length% — everyone knows everyone.', { length: arr.length }); }
else if (c.ind) { st.className = 'status ind'; st.textContent = fmt('✓ Independent set of %length% — all strangers.', { length: arr.length }); }
else { st.className = 'status no'; st.textContent = 'Neither — some pairs connected, some not.'; }
}
function best(want) {
let bestSet = [];
for (let m = 0; m < (1 << N); m++) {
const arr = []; for (let i = 0; i < N; i++) if (m & (1 << i)) arr.push(i);
const c = classify(arr);
if ((want === 'clq' ? c.clq : c.ind) && arr.length > bestSet.length) bestSet = arr;
}
sel = new Set(bestSet); mode = want; render();
const st = document.getElementById('status');
st.className = 'status ' + want;
st.textContent = fmt('%v% has %length% people: {%bestSet%}.', { v: want === 'clq' ? '★ Maximum clique' : '★ Maximum independent set', length: bestSet.length, bestSet: bestSet.join(', ') });
}
document.getElementById('mc').addEventListener('click', () => best('clq'));
document.getElementById('mi').addEventListener('click', () => best('ind'));
document.getElementById('reset').addEventListener('click', () => { sel = new Set(); mode = ''; render(); });
render();
<p class="hint">Click people to select a group. A <b>clique</b> = everyone knows everyone (all connected). An <b>independent set</b> = total strangers (none connected). The checker tells you live which one you've got.</p>
<svg id="g" viewBox="0 0 320 230" class="g"></svg>
<div id="status" class="status"></div>
<div class="btns">
<button id="mc" type="button">Max clique</button>
<button id="mi" type="button">Max independent set</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 .7rem; line-height: 1.45; }
.g { width: 100%; max-width: 460px; background: #f4f7f9; border: 1px solid #e2e6eb; border-radius: 10px; display: block; }
.edge { stroke: #c2ccd6; stroke-width: 2.5; }
.node { fill: #8aa0b3; cursor: pointer; transition: fill .1s; }
.node:hover { fill: #6f8699; }
.node.sel { fill: #457b9d; }
.node.clq { fill: #2a9d8f; }
.node.ind { fill: #b5651d; }
.nlbl { font: 700 12px ui-monospace, monospace; fill: #fff; text-anchor: middle; dominant-baseline: central; pointer-events: none; }
.status { font-weight: 800; min-height: 1.4em; margin: .8rem 0 .5rem; font-size: 1rem; }
.status.clq { color: #0a7d33; } .status.ind { color: #b5651d; } .status.no { color: #777; }
.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] ?? ''));
// Clique = all pairs connected; independent set = no pair connected.
// Both are NP-hard (and hard to approximate). Here we brute-force the maxima.
const N = 7;
const POS = [[110, 60], [210, 60], [210, 150], [110, 150], [40, 30], [285, 35], [285, 180]];
const E = [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3], [0, 4], [1, 5], [2, 6], [4, 5]];
const adj = Array.from({ length: N }, () => new Set());
E.forEach(([u, v]) => { adj[u].add(v); adj[v].add(u); });
let sel = new Set(), mode = '';
function classify(arr) {
let clq = true, ind = true;
for (let a = 0; a < arr.length; a++) for (let b = a + 1; b < arr.length; b++) {
if (adj[arr[a]].has(arr[b])) ind = false; else clq = false;
}
return { clq: clq && arr.length > 1, ind: ind && arr.length > 1 };
}
function render() {
const arr = [...sel];
const c = classify(arr);
let s = '';
E.forEach(([u, v]) => { s += '<line class="edge" x1="' + POS[u][0] + '" y1="' + POS[u][1] + '" x2="' + POS[v][0] + '" y2="' + POS[v][1] + '"/>'; });
for (let i = 0; i < N; i++) {
let cls = 'node';
if (sel.has(i)) cls += mode === 'clq' ? ' clq' : (mode === 'ind' ? ' ind' : ' sel');
s += '<circle class="' + cls + '" cx="' + POS[i][0] + '" cy="' + POS[i][1] + '" r="16" data-i="' + i + '"/>';
s += '<text class="nlbl" x="' + POS[i][0] + '" y="' + POS[i][1] + '">' + i + '</text>';
}
const svg = document.getElementById('g');
svg.innerHTML = s;
svg.querySelectorAll('.node').forEach(n => n.addEventListener('click', () => { mode = ''; const i = +n.dataset.i; sel.has(i) ? sel.delete(i) : sel.add(i); render(); }));
const st = document.getElementById('status');
if (arr.length < 2) { st.className = 'status no'; st.textContent = 'Select at least two people.'; }
else if (c.clq) { st.className = 'status clq'; st.textContent = fmt('✓ Clique of %length% — everyone knows everyone.', { length: arr.length }); }
else if (c.ind) { st.className = 'status ind'; st.textContent = fmt('✓ Independent set of %length% — all strangers.', { length: arr.length }); }
else { st.className = 'status no'; st.textContent = 'Neither — some pairs connected, some not.'; }
}
function best(want) {
let bestSet = [];
for (let m = 0; m < (1 << N); m++) {
const arr = []; for (let i = 0; i < N; i++) if (m & (1 << i)) arr.push(i);
const c = classify(arr);
if ((want === 'clq' ? c.clq : c.ind) && arr.length > bestSet.length) bestSet = arr;
}
sel = new Set(bestSet); mode = want; render();
const st = document.getElementById('status');
st.className = 'status ' + want;
st.textContent = fmt('%v% has %length% people: {%bestSet%}.', { v: want === 'clq' ? '★ Maximum clique' : '★ Maximum independent set', length: bestSet.length, bestSet: bestSet.join(', ') });
}
document.getElementById('mc').addEventListener('click', () => best('clq'));
document.getElementById('mi').addEventListener('click', () => best('ind'));
document.getElementById('reset').addEventListener('click', () => { sel = new Set(); mode = ''; render(); });
render();
Hunt for the biggest clique and the biggest independent set by hand, then press the buttons to reveal the true maxima (found by brute force). Notice they're different sizes — and that finding them got harder the moment the graph grew past a handful of nodes.
Comments
Loading comments...