<p class="hint">This graph hides a <b>K5</b> minor — five groups of vertices, every pair linked. Click an edge to select it, then <b>Contract</b> to merge its two endpoints into one super-vertex, or <b>Delete</b> to remove it. Reach five super-vertices that are all pairwise connected and you've pulled out a K5.</p>
<svg id="graph" viewBox="0 0 320 300" aria-label="graph"></svg>
<div class="status" id="status">Select an edge to begin.</div>
<div class="btns">
<button id="contract" type="button">Contract edge</button>
<button id="delete" type="button">Delete edge</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 .6rem; line-height: 1.45; }
svg { width: 100%; max-width: 320px; height: auto; display: block; margin: 0 auto; touch-action: manipulation; }
.edge { stroke: #8a93a0; stroke-width: 3; cursor: pointer; }
.edge:hover { stroke: #1d3557; }
.edge.sel { stroke: #e63946; stroke-width: 5; }
.node { fill: #1d3557; stroke: #fff; stroke-width: 2; }
.node-label { fill: #fff; font: 700 11px ui-monospace, monospace; text-anchor: middle; dominant-baseline: central; pointer-events: none; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; 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] ?? ''));
// Graph-minor extraction. We start from a graph that CONTAINS a K5 minor but is not itself K5: it has 5 "core" vertices forming a near-complete graph, plus an extra vertex 5 that sits on the path between cores 0 and 4, plus one redundant edge. The player must CONTRACT to merge the extra vertex into a core (restoring the missing direct link) and may DELETE leftover edges. A K5 minor is exposed when the contracted graph has exactly 5 super-vertices that are pairwise adjacent.
//
// Minor operations on a simple graph:
// delete(u,v) : remove the edge
// contract(u,v) : merge v into u; reroute v's edges to u; drop self-loops & parallels
const START = {
// positions for layout (id -> [x,y])
pos: { 0:[160,40], 1:[280,120], 2:[235,260], 3:[85,260], 4:[40,120], 5:[100,150] },
// initial edges (undirected, stored as "min,max")
edges: [[0,1],[0,2],[0,3],[1,2],[1,3],[1,4],[2,3],[2,4],[3,4],[4,5],[5,0],[5,3]]
};
let nodes, adj, members, selected;
const svg = document.getElementById('graph');
function key(a,b){ return a<b ? a+','+b : b+','+a; }
function init(){
nodes = Object.keys(START.pos).map(Number);
members = {}; nodes.forEach(n => members[n] = [n]); // which original ids each super-node holds
adj = {}; nodes.forEach(n => adj[n] = new Set());
START.edges.forEach(([a,b]) => { adj[a].add(b); adj[b].add(a); });
selected = null;
render();
setStatus('Select an edge to begin.', '');
}
function pos(n){
// a super-node sits at the average of its members' original positions
const ms = members[n]; let x=0,y=0;
ms.forEach(m => { x += START.pos[m][0]; y += START.pos[m][1]; });
return [x/ms.length, y/ms.length];
}
function edgeList(){
const out = []; const seen = new Set();
nodes.forEach(u => adj[u].forEach(v => { const k = key(u,v); if(!seen.has(k)){ seen.add(k); out.push([Math.min(u,v),Math.max(u,v)]); } }));
return out;
}
function render(){
svg.innerHTML = '';
// edges
edgeList().forEach(([a,b]) => {
const [x1,y1]=pos(a), [x2,y2]=pos(b);
const ln = document.createElementNS('http://www.w3.org/2000/svg','line');
ln.setAttribute('x1',x1); ln.setAttribute('y1',y1); ln.setAttribute('x2',x2); ln.setAttribute('y2',y2);
ln.setAttribute('class','edge'+(selected && key(a,b)===selected ? ' sel':''));
ln.addEventListener('click',()=>{ selected = key(a,b); render(); setStatus('Edge selected. Contract or delete it.',''); });
svg.appendChild(ln);
});
// nodes
nodes.forEach(n => {
const [x,y]=pos(n);
const c = document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',x); c.setAttribute('cy',y); c.setAttribute('r',16); c.setAttribute('class','node');
svg.appendChild(c);
const t = document.createElementNS('http://www.w3.org/2000/svg','text');
t.setAttribute('x',x); t.setAttribute('y',y); t.setAttribute('class','node-label');
t.textContent = members[n].slice().sort((a,b)=>a-b).join('');
svg.appendChild(t);
});
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
function selectedPair(){
if(!selected) return null;
const [a,b] = selected.split(',').map(Number);
if(adj[a] && adj[a].has(b)) return [a,b];
return null;
}
function del(){
const p = selectedPair();
if(!p){ setStatus('Select an edge first.','bad'); return; }
const [a,b]=p; adj[a].delete(b); adj[b].delete(a); selected=null; render(); checkWin('Edge deleted.');
}
function contract(){
const p = selectedPair();
if(!p){ setStatus('Select an edge first.','bad'); return; }
const [u,v]=p; // merge v into u
adj[v].forEach(w => { if(w!==u){ adj[u].add(w); adj[w].add(v); adj[w].delete(v); adj[w].add(u); } });
adj[u].delete(v); // drop the contracted edge / self-loop
members[u] = members[u].concat(members[v]);
delete adj[v];
nodes = nodes.filter(n => n!==v);
selected=null; render(); checkWin('Edge contracted.');
}
// K5 minor exposed <=> exactly 5 super-vertices, all pairwise adjacent (10 edges).
function isK5(){
if(nodes.length !== 5) return false;
for(let i=0;i<nodes.length;i++) for(let j=i+1;j<nodes.length;j++)
if(!adj[nodes[i]].has(nodes[j])) return false;
return true;
}
function checkWin(prefix){
if(isK5()) setStatus('K5 minor exposed — five super-vertices, all pairwise linked!','ok');
else if(nodes.length < 5) setStatus(fmt('%prefix% Too few vertices now — you contracted too much. Reset to retry.', { prefix: prefix }),'bad');
else setStatus(fmt('%prefix% Keep going: reach 5 vertices that are all pairwise connected.', { prefix: prefix }),'');
}
document.getElementById('contract').addEventListener('click',contract);
document.getElementById('delete').addEventListener('click',del);
document.getElementById('reset').addEventListener('click',init);
init();
<p class="hint">This graph hides a <b>K5</b> minor — five groups of vertices, every pair linked. Click an edge to select it, then <b>Contract</b> to merge its two endpoints into one super-vertex, or <b>Delete</b> to remove it. Reach five super-vertices that are all pairwise connected and you've pulled out a K5.</p>
<svg id="graph" viewBox="0 0 320 300" aria-label="graph"></svg>
<div class="status" id="status">Select an edge to begin.</div>
<div class="btns">
<button id="contract" type="button">Contract edge</button>
<button id="delete" type="button">Delete edge</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 .6rem; line-height: 1.45; }
svg { width: 100%; max-width: 320px; height: auto; display: block; margin: 0 auto; touch-action: manipulation; }
.edge { stroke: #8a93a0; stroke-width: 3; cursor: pointer; }
.edge:hover { stroke: #1d3557; }
.edge.sel { stroke: #e63946; stroke-width: 5; }
.node { fill: #1d3557; stroke: #fff; stroke-width: 2; }
.node-label { fill: #fff; font: 700 11px ui-monospace, monospace; text-anchor: middle; dominant-baseline: central; pointer-events: none; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; 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] ?? ''));
// Graph-minor extraction. We start from a graph that CONTAINS a K5 minor but is not itself K5: it has 5 "core" vertices forming a near-complete graph, plus an extra vertex 5 that sits on the path between cores 0 and 4, plus one redundant edge. The player must CONTRACT to merge the extra vertex into a core (restoring the missing direct link) and may DELETE leftover edges. A K5 minor is exposed when the contracted graph has exactly 5 super-vertices that are pairwise adjacent.
//
// Minor operations on a simple graph:
// delete(u,v) : remove the edge
// contract(u,v) : merge v into u; reroute v's edges to u; drop self-loops & parallels
const START = {
// positions for layout (id -> [x,y])
pos: { 0:[160,40], 1:[280,120], 2:[235,260], 3:[85,260], 4:[40,120], 5:[100,150] },
// initial edges (undirected, stored as "min,max")
edges: [[0,1],[0,2],[0,3],[1,2],[1,3],[1,4],[2,3],[2,4],[3,4],[4,5],[5,0],[5,3]]
};
let nodes, adj, members, selected;
const svg = document.getElementById('graph');
function key(a,b){ return a<b ? a+','+b : b+','+a; }
function init(){
nodes = Object.keys(START.pos).map(Number);
members = {}; nodes.forEach(n => members[n] = [n]); // which original ids each super-node holds
adj = {}; nodes.forEach(n => adj[n] = new Set());
START.edges.forEach(([a,b]) => { adj[a].add(b); adj[b].add(a); });
selected = null;
render();
setStatus('Select an edge to begin.', '');
}
function pos(n){
// a super-node sits at the average of its members' original positions
const ms = members[n]; let x=0,y=0;
ms.forEach(m => { x += START.pos[m][0]; y += START.pos[m][1]; });
return [x/ms.length, y/ms.length];
}
function edgeList(){
const out = []; const seen = new Set();
nodes.forEach(u => adj[u].forEach(v => { const k = key(u,v); if(!seen.has(k)){ seen.add(k); out.push([Math.min(u,v),Math.max(u,v)]); } }));
return out;
}
function render(){
svg.innerHTML = '';
// edges
edgeList().forEach(([a,b]) => {
const [x1,y1]=pos(a), [x2,y2]=pos(b);
const ln = document.createElementNS('http://www.w3.org/2000/svg','line');
ln.setAttribute('x1',x1); ln.setAttribute('y1',y1); ln.setAttribute('x2',x2); ln.setAttribute('y2',y2);
ln.setAttribute('class','edge'+(selected && key(a,b)===selected ? ' sel':''));
ln.addEventListener('click',()=>{ selected = key(a,b); render(); setStatus('Edge selected. Contract or delete it.',''); });
svg.appendChild(ln);
});
// nodes
nodes.forEach(n => {
const [x,y]=pos(n);
const c = document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',x); c.setAttribute('cy',y); c.setAttribute('r',16); c.setAttribute('class','node');
svg.appendChild(c);
const t = document.createElementNS('http://www.w3.org/2000/svg','text');
t.setAttribute('x',x); t.setAttribute('y',y); t.setAttribute('class','node-label');
t.textContent = members[n].slice().sort((a,b)=>a-b).join('');
svg.appendChild(t);
});
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
function selectedPair(){
if(!selected) return null;
const [a,b] = selected.split(',').map(Number);
if(adj[a] && adj[a].has(b)) return [a,b];
return null;
}
function del(){
const p = selectedPair();
if(!p){ setStatus('Select an edge first.','bad'); return; }
const [a,b]=p; adj[a].delete(b); adj[b].delete(a); selected=null; render(); checkWin('Edge deleted.');
}
function contract(){
const p = selectedPair();
if(!p){ setStatus('Select an edge first.','bad'); return; }
const [u,v]=p; // merge v into u
adj[v].forEach(w => { if(w!==u){ adj[u].add(w); adj[w].add(v); adj[w].delete(v); adj[w].add(u); } });
adj[u].delete(v); // drop the contracted edge / self-loop
members[u] = members[u].concat(members[v]);
delete adj[v];
nodes = nodes.filter(n => n!==v);
selected=null; render(); checkWin('Edge contracted.');
}
// K5 minor exposed <=> exactly 5 super-vertices, all pairwise adjacent (10 edges).
function isK5(){
if(nodes.length !== 5) return false;
for(let i=0;i<nodes.length;i++) for(let j=i+1;j<nodes.length;j++)
if(!adj[nodes[i]].has(nodes[j])) return false;
return true;
}
function checkWin(prefix){
if(isK5()) setStatus('K5 minor exposed — five super-vertices, all pairwise linked!','ok');
else if(nodes.length < 5) setStatus(fmt('%prefix% Too few vertices now — you contracted too much. Reset to retry.', { prefix: prefix }),'bad');
else setStatus(fmt('%prefix% Keep going: reach 5 vertices that are all pairwise connected.', { prefix: prefix }),'');
}
document.getElementById('contract').addEventListener('click',contract);
document.getElementById('delete').addEventListener('click',del);
document.getElementById('reset').addEventListener('click',init);
init();
Comments
Loading comments...