Here is a small social network. A clique is a group of people who all know each other — every pair connected. The goal is to find the largest clique. Click Greedy guess to run a fast heuristic that repeatedly grabs the most-connected vertex, then Find the true maximum to brute-force the real answer.
<p class="hint">A <b>clique</b> is a group where everyone is connected to everyone else. The goal: find the <b>largest</b> clique. The greedy heuristic is instant but can fall short — the true maximum needs an exhaustive search. Watch the <b>gap</b> between "good enough" and "best".</p>
<svg id="graph" viewBox="0 0 300 240" class="graph"></svg>
<div class="scores">
<div class="score"><span class="lbl">Greedy guess</span><span id="greedy" class="val">—</span></div>
<div class="score"><span class="lbl">True maximum</span><span id="optimum" class="val">—</span></div>
</div>
<div class="status" id="status">Run the greedy heuristic, then find the true maximum.</div>
<div class="btns">
<button id="greedyBtn" type="button">Greedy guess</button>
<button id="optBtn" type="button">Find the true maximum</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 .6rem; line-height: 1.45; }
.graph { width: 100%; max-width: 360px; height: auto; display: block; margin: 0 auto .4rem;
background: #f5f7fa; border: 1px solid #dde4ec; border-radius: 10px; }
.edge { stroke: #b9c4d0; stroke-width: 2; }
.edge.in { stroke: #1d3557; stroke-width: 3.5; }
.node { fill: #c9ccd1; stroke: #adb1b8; stroke-width: 1.5; }
.node.in { fill: #e63946; stroke: #c92f3c; }
.nlabel { font: 700 11px ui-monospace, monospace; text-anchor: middle; pointer-events: none; }
.scores { display: flex; gap: .6rem; margin: .3rem 0; flex-wrap: wrap; }
.score { flex: 1; min-width: 120px; background: #eef2f6; border: 1px solid #d6dee7;
border-radius: 8px; padding: .4rem .6rem; display: flex; justify-content: space-between; align-items: center; }
.score .lbl { font-size: .82rem; color: #45566a; }
.score .val { font: 800 20px ui-monospace, monospace; color: #1d3557; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.gap { 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] ?? ''));
// Maximum Clique. A clique = a set of vertices all pairwise connected.
// GREEDY: start from the highest-degree vertex, then keep adding the next
// vertex that stays adjacent to everyone chosen. Fast, but no guarantee.
// OPTIMUM: brute force over all 2^n subsets — the true largest clique.
// The gap is the point: approximating max-clique within n^(1-e) is NP-hard
// (Hastad, 1996), powered by the PCP theorem (1992). No fast algorithm closes it.
// 12 vertices. The 5-clique {4,5,6,7,8} is the true maximum. But the hub
// vertices {0,1,2} have the highest degree (5), so greedy dives into them and
// gets stuck at a 4-clique {0,1,2,10} — one short of the real answer.
const POS = [
[60,55],[150,40],[240,55], // 0,1,2 hubs
[150,95], // 3
[70,150],[150,135],[230,150], // 4,5,6 big clique
[95,210],[205,210], // 7,8 big clique
[25,95],[150,205],[275,95] // 9,10,11 leaves
];
const E = [
[4,5],[4,6],[4,7],[4,8],[5,6],[5,7],[5,8],[6,7],[6,8],[7,8], // K5 on {4,5,6,7,8}
[0,1],[1,2],[0,2], // hub triangle
[0,9],[0,10],[0,11],
[1,9],[1,10],[1,3],
[2,10],[2,11],[2,3]
];
const N = POS.length;
const adj = Array.from({length:N}, () => new Set());
E.forEach(([a,b]) => { adj[a].add(b); adj[b].add(a); });
const connected = (u,v) => adj[u].has(v);
const isClique = s => { for(let i=0;i<s.length;i++) for(let j=i+1;j<s.length;j++) if(!connected(s[i],s[j])) return false; return true; };
function greedyClique(){
const order = [...Array(N).keys()].sort((a,b)=>adj[b].size-adj[a].size);
const clique = [order[0]];
for(const v of order){
if(clique.includes(v)) continue;
if(clique.every(u=>connected(u,v))) clique.push(v);
}
return clique;
}
function maxClique(){
let best=[];
for(let mask=1; mask<(1<<N); mask++){
const s=[]; for(let i=0;i<N;i++) if(mask&(1<<i)) s.push(i);
if(s.length>best.length && isClique(s)) best=s;
}
return best;
}
let current=[];
function draw(){
const svg=document.getElementById('graph'); svg.innerHTML='';
const inSet=new Set(current);
const inBoth=(a,b)=>inSet.has(a)&&inSet.has(b);
E.forEach(([a,b])=>{
const l=document.createElementNS('http://www.w3.org/2000/svg','line');
l.setAttribute('x1',POS[a][0]);l.setAttribute('y1',POS[a][1]);
l.setAttribute('x2',POS[b][0]);l.setAttribute('y2',POS[b][1]);
l.setAttribute('class','edge'+(inBoth(a,b)?' in':''));
svg.appendChild(l);
});
POS.forEach(([x,y],i)=>{
const c=document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',x);c.setAttribute('cy',y);c.setAttribute('r',13);
c.setAttribute('class','node'+(inSet.has(i)?' in':''));
svg.appendChild(c);
const t=document.createElementNS('http://www.w3.org/2000/svg','text');
t.setAttribute('x',x);t.setAttribute('y',y+4);t.setAttribute('class','nlabel');
t.setAttribute('fill',inSet.has(i)?'#fff':'#1d3557');
t.textContent=i; svg.appendChild(t);
});
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
document.getElementById('greedyBtn').addEventListener('click',()=>{
current=greedyClique();
document.getElementById('greedy').textContent=current.length;
draw();
setStatus(fmt('Greedy found a clique of size %length% ({%sort%}). Is it the biggest?', { length: current.length, sort: current.slice().sort((a,b)=>a-b).join(',') }),'');
console.log('Greedy clique:', current.join(','), 'size', current.length);
});
document.getElementById('optBtn').addEventListener('click',()=>{
const opt=maxClique();
document.getElementById('optimum').textContent=opt.length;
const g=document.getElementById('greedy').textContent;
current=opt; draw();
if(g!=='—' && +g<opt.length) setStatus(fmt('The true maximum is %length% — greedy missed by %v%. That gap is inapproximability.', { length: opt.length, v: opt.length-+g }),'gap');
else setStatus(fmt('The true maximum clique has size %length% ({%sort%}).', { length: opt.length, sort: opt.slice().sort((a,b)=>a-b).join(',') }),'ok');
console.log('Max clique:', opt.join(','), 'size', opt.length);
});
document.getElementById('reset').addEventListener('click',()=>{
current=[]; document.getElementById('greedy').textContent='—';
document.getElementById('optimum').textContent='—';
draw(); setStatus('Run the greedy heuristic, then find the true maximum.','');
});
draw();
<p class="hint">A <b>clique</b> is a group where everyone is connected to everyone else. The goal: find the <b>largest</b> clique. The greedy heuristic is instant but can fall short — the true maximum needs an exhaustive search. Watch the <b>gap</b> between "good enough" and "best".</p>
<svg id="graph" viewBox="0 0 300 240" class="graph"></svg>
<div class="scores">
<div class="score"><span class="lbl">Greedy guess</span><span id="greedy" class="val">—</span></div>
<div class="score"><span class="lbl">True maximum</span><span id="optimum" class="val">—</span></div>
</div>
<div class="status" id="status">Run the greedy heuristic, then find the true maximum.</div>
<div class="btns">
<button id="greedyBtn" type="button">Greedy guess</button>
<button id="optBtn" type="button">Find the true maximum</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 .6rem; line-height: 1.45; }
.graph { width: 100%; max-width: 360px; height: auto; display: block; margin: 0 auto .4rem;
background: #f5f7fa; border: 1px solid #dde4ec; border-radius: 10px; }
.edge { stroke: #b9c4d0; stroke-width: 2; }
.edge.in { stroke: #1d3557; stroke-width: 3.5; }
.node { fill: #c9ccd1; stroke: #adb1b8; stroke-width: 1.5; }
.node.in { fill: #e63946; stroke: #c92f3c; }
.nlabel { font: 700 11px ui-monospace, monospace; text-anchor: middle; pointer-events: none; }
.scores { display: flex; gap: .6rem; margin: .3rem 0; flex-wrap: wrap; }
.score { flex: 1; min-width: 120px; background: #eef2f6; border: 1px solid #d6dee7;
border-radius: 8px; padding: .4rem .6rem; display: flex; justify-content: space-between; align-items: center; }
.score .lbl { font-size: .82rem; color: #45566a; }
.score .val { font: 800 20px ui-monospace, monospace; color: #1d3557; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.gap { 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] ?? ''));
// Maximum Clique. A clique = a set of vertices all pairwise connected.
// GREEDY: start from the highest-degree vertex, then keep adding the next
// vertex that stays adjacent to everyone chosen. Fast, but no guarantee.
// OPTIMUM: brute force over all 2^n subsets — the true largest clique.
// The gap is the point: approximating max-clique within n^(1-e) is NP-hard
// (Hastad, 1996), powered by the PCP theorem (1992). No fast algorithm closes it.
// 12 vertices. The 5-clique {4,5,6,7,8} is the true maximum. But the hub
// vertices {0,1,2} have the highest degree (5), so greedy dives into them and
// gets stuck at a 4-clique {0,1,2,10} — one short of the real answer.
const POS = [
[60,55],[150,40],[240,55], // 0,1,2 hubs
[150,95], // 3
[70,150],[150,135],[230,150], // 4,5,6 big clique
[95,210],[205,210], // 7,8 big clique
[25,95],[150,205],[275,95] // 9,10,11 leaves
];
const E = [
[4,5],[4,6],[4,7],[4,8],[5,6],[5,7],[5,8],[6,7],[6,8],[7,8], // K5 on {4,5,6,7,8}
[0,1],[1,2],[0,2], // hub triangle
[0,9],[0,10],[0,11],
[1,9],[1,10],[1,3],
[2,10],[2,11],[2,3]
];
const N = POS.length;
const adj = Array.from({length:N}, () => new Set());
E.forEach(([a,b]) => { adj[a].add(b); adj[b].add(a); });
const connected = (u,v) => adj[u].has(v);
const isClique = s => { for(let i=0;i<s.length;i++) for(let j=i+1;j<s.length;j++) if(!connected(s[i],s[j])) return false; return true; };
function greedyClique(){
const order = [...Array(N).keys()].sort((a,b)=>adj[b].size-adj[a].size);
const clique = [order[0]];
for(const v of order){
if(clique.includes(v)) continue;
if(clique.every(u=>connected(u,v))) clique.push(v);
}
return clique;
}
function maxClique(){
let best=[];
for(let mask=1; mask<(1<<N); mask++){
const s=[]; for(let i=0;i<N;i++) if(mask&(1<<i)) s.push(i);
if(s.length>best.length && isClique(s)) best=s;
}
return best;
}
let current=[];
function draw(){
const svg=document.getElementById('graph'); svg.innerHTML='';
const inSet=new Set(current);
const inBoth=(a,b)=>inSet.has(a)&&inSet.has(b);
E.forEach(([a,b])=>{
const l=document.createElementNS('http://www.w3.org/2000/svg','line');
l.setAttribute('x1',POS[a][0]);l.setAttribute('y1',POS[a][1]);
l.setAttribute('x2',POS[b][0]);l.setAttribute('y2',POS[b][1]);
l.setAttribute('class','edge'+(inBoth(a,b)?' in':''));
svg.appendChild(l);
});
POS.forEach(([x,y],i)=>{
const c=document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',x);c.setAttribute('cy',y);c.setAttribute('r',13);
c.setAttribute('class','node'+(inSet.has(i)?' in':''));
svg.appendChild(c);
const t=document.createElementNS('http://www.w3.org/2000/svg','text');
t.setAttribute('x',x);t.setAttribute('y',y+4);t.setAttribute('class','nlabel');
t.setAttribute('fill',inSet.has(i)?'#fff':'#1d3557');
t.textContent=i; svg.appendChild(t);
});
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
document.getElementById('greedyBtn').addEventListener('click',()=>{
current=greedyClique();
document.getElementById('greedy').textContent=current.length;
draw();
setStatus(fmt('Greedy found a clique of size %length% ({%sort%}). Is it the biggest?', { length: current.length, sort: current.slice().sort((a,b)=>a-b).join(',') }),'');
console.log('Greedy clique:', current.join(','), 'size', current.length);
});
document.getElementById('optBtn').addEventListener('click',()=>{
const opt=maxClique();
document.getElementById('optimum').textContent=opt.length;
const g=document.getElementById('greedy').textContent;
current=opt; draw();
if(g!=='—' && +g<opt.length) setStatus(fmt('The true maximum is %length% — greedy missed by %v%. That gap is inapproximability.', { length: opt.length, v: opt.length-+g }),'gap');
else setStatus(fmt('The true maximum clique has size %length% ({%sort%}).', { length: opt.length, sort: opt.slice().sort((a,b)=>a-b).join(',') }),'ok');
console.log('Max clique:', opt.join(','), 'size', opt.length);
});
document.getElementById('reset').addEventListener('click',()=>{
current=[]; document.getElementById('greedy').textContent='—';
document.getElementById('optimum').textContent='—';
draw(); setStatus('Run the greedy heuristic, then find the true maximum.','');
});
draw();
Comments
Loading comments...