<p class="hint">Left: a small <b>pattern</b> graph. Right: a larger <b>target</b>. The matcher maps each pattern vertex to a target vertex so that every pattern edge becomes a real target edge. Press <b>Search</b> to watch it assign vertices and <b>backtrack</b>. Verifying a map is instant; finding one can blow up.</p>
<div class="stage">
<div class="panel">
<div class="cap">Pattern</div>
<svg id="pat" viewBox="0 0 160 160"></svg>
</div>
<div class="panel">
<div class="cap">Target</div>
<svg id="tgt" viewBox="0 0 240 200"></svg>
</div>
</div>
<div class="status" id="status">Press Search to hunt the pattern.</div>
<div class="btns">
<button id="search" type="button">Search</button>
<button id="shuffle" type="button" class="ghost">Shuffle target</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; }
.stage { display: flex; gap: 1rem; flex-wrap: wrap; }
.panel { background: #f3f6f9; border: 1px solid #d8e0e8; border-radius: 10px; padding: .5rem; }
.cap { font: 700 13px system-ui; color: #1d3557; margin-bottom: .2rem; }
svg { display: block; }
.edge { stroke: #adb5bd; stroke-width: 3; }
.edge.lit { stroke: #0a7d33; stroke-width: 4; }
.node { fill: #c9ccd1; stroke: #8b929b; stroke-width: 2; }
.node.mapped { fill: #1d3557; stroke: #122440; }
.node.cur { fill: #e63946; stroke: #c92f3c; }
.nlabel { font: 700 13px ui-monospace, monospace; fill: #fff; text-anchor: middle; dominant-baseline: central; }
.nlabel.dark { fill: #1d3557; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 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; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .5; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Subgraph isomorphism: can the PATTERN be embedded in the TARGET so every
// pattern edge maps to a target edge? Checking a map is a quick edge scan;
// finding one is NP-complete. We use backtracking (Ullmann-style) and animate it.
// Pattern: a triangle 0-1-2 plus a tail 2-3 (a "kite handle").
const PN = 4;
const PE = [[0,1],[1,2],[2,0],[2,3]];
const PPOS = [[40,40],[120,40],[80,100],[80,150]];
// Target: 6 vertices. Layout fixed; adjacency reshuffled to vary difficulty.
const TN = 6;
const TPOS = [[40,40],[120,30],[200,55],[60,120],[150,120],[210,150]];
let TE = []; // target edges
let adj = []; // adjacency matrix
function buildAdj(){
adj = Array.from({length:TN},()=>Array(TN).fill(false));
for(const [a,b] of TE){ adj[a][b]=true; adj[b][a]=true; }
}
const padj = Array.from({length:PN},()=>Array(PN).fill(false));
for(const [a,b] of PE){ padj[a][b]=true; padj[b][a]=true; }
function makeTarget(){
// Guarantee at least one embedding exists: pick 4 distinct target vertices
// as a planted copy of the pattern, then add a few random extra edges.
const perm = [...Array(TN).keys()];
for(let i=perm.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [perm[i],perm[j]]=[perm[j],perm[i]]; }
const set = new Set();
const add=(a,b)=>{ const k=a<b?a+'-'+b:b+'-'+a; set.add(k); };
for(const [a,b] of PE) add(perm[a], perm[b]); // planted pattern
// sprinkle random noise edges
let tries=0;
while(set.size < PE.length+3 && tries<40){ tries++;
const a=Math.floor(Math.random()*TN), b=Math.floor(Math.random()*TN);
if(a!==b) add(a,b);
}
TE = [...set].map(s=>s.split('-').map(Number));
buildAdj();
}
// ── Backtracking matcher. Returns {ok, map, steps}. map[p] = target vertex. ──
function solve(){
const map = Array(PN).fill(-1);
const used = Array(TN).fill(false);
let steps = 0;
function consistent(p, t){
for(let q=0;q<p;q++){ if(padj[p][q] && !adj[t][map[q]]) return false; }
return true;
}
function bt(p){
if(p===PN) return true;
for(let t=0;t<TN;t++){ steps++;
if(used[t]) continue;
if(!consistent(p,t)) continue;
map[p]=t; used[t]=true;
if(bt(p+1)) return true;
map[p]=-1; used[t]=false;
}
return false;
}
const ok = bt(0);
return { ok, map: ok?map.slice():null, steps };
}
// ── Rendering ──
function drawGraph(svg, n, edges, pos, mapInfo){
svg.innerHTML='';
const NS='http://www.w3.org/2000/svg';
for(const [a,b] of edges){
const l=document.createElementNS(NS,'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'+(mapInfo&&mapInfo.litEdges&&mapInfo.litEdges.has((a<b?a+'-'+b:b+'-'+a))?' lit':''));
svg.appendChild(l);
}
for(let i=0;i<n;i++){
const c=document.createElementNS(NS,'circle');
c.setAttribute('cx',pos[i][0]); c.setAttribute('cy',pos[i][1]); c.setAttribute('r',16);
let cls='node';
if(mapInfo){ if(mapInfo.cur===i) cls+=' cur'; else if(mapInfo.mapped&&mapInfo.mapped.has(i)) cls+=' mapped'; }
c.setAttribute('class',cls);
svg.appendChild(c);
const t=document.createElementNS(NS,'text');
t.setAttribute('x',pos[i][0]); t.setAttribute('y',pos[i][1]);
t.setAttribute('class','nlabel'+(cls==='node'?' dark':''));
t.textContent=String.fromCharCode((svg.id==='pat'?80:65)+i); // P:A.. / T:A..
svg.appendChild(t);
}
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
function renderStatic(){
drawGraph(document.getElementById('pat'), PN, PE, PPOS, null);
drawGraph(document.getElementById('tgt'), TN, TE, TPOS, null);
}
function animateResult(res){
const btn=document.getElementById('search'); btn.disabled=true;
if(!res.ok){
setStatus(fmt('No embedding exists (checked %steps% assignments).', { steps: res.steps }),'bad');
btn.disabled=false; return;
}
const mappedT=new Set(), mappedP=new Set();
let i=0;
const timer=setInterval(()=>{
if(i>=PN){ clearInterval(timer);
// light up matched edges on both graphs
const lit=new Set();
for(const [a,b] of PE){ const ta=res.map[a], tb=res.map[b]; lit.add(ta<tb?ta+'-'+tb:tb+'-'+ta); }
drawGraph(document.getElementById('tgt'), TN, TE, TPOS, {mapped:mappedT, litEdges:lit});
const litP=new Set(); for(const [a,b] of PE) litP.add(a<b?a+'-'+b:b+'-'+a);
drawGraph(document.getElementById('pat'), PN, PE, PPOS, {mapped:mappedP, litEdges:litP});
setStatus(fmt('Found! Pattern embeds via %map% (%steps% steps).', { map: res.map.map((t,p)=>String.fromCharCode(80+p)+'→'+String.fromCharCode(65+t)).join(', '), steps: res.steps }),'ok');
console.log('mapping', res.map, 'steps', res.steps);
btn.disabled=false; return;
}
const t=res.map[i];
drawGraph(document.getElementById('pat'), PN, PE, PPOS, {cur:i, mapped:new Set(mappedP)});
drawGraph(document.getElementById('tgt'), TN, TE, TPOS, {cur:t, mapped:new Set(mappedT)});
setStatus(fmt('Mapping %fromCharCode% → %fromCharCode2% …', { fromCharCode: String.fromCharCode(80+i), fromCharCode2: String.fromCharCode(65+t) }),'');
mappedP.add(i); mappedT.add(t); i++;
}, 650);
}
document.getElementById('search').addEventListener('click',()=>animateResult(solve()));
document.getElementById('shuffle').addEventListener('click',()=>{ makeTarget(); renderStatic(); setStatus('Target reshuffled. Press Search.',''); });
makeTarget(); renderStatic();
<p class="hint">Left: a small <b>pattern</b> graph. Right: a larger <b>target</b>. The matcher maps each pattern vertex to a target vertex so that every pattern edge becomes a real target edge. Press <b>Search</b> to watch it assign vertices and <b>backtrack</b>. Verifying a map is instant; finding one can blow up.</p>
<div class="stage">
<div class="panel">
<div class="cap">Pattern</div>
<svg id="pat" viewBox="0 0 160 160"></svg>
</div>
<div class="panel">
<div class="cap">Target</div>
<svg id="tgt" viewBox="0 0 240 200"></svg>
</div>
</div>
<div class="status" id="status">Press Search to hunt the pattern.</div>
<div class="btns">
<button id="search" type="button">Search</button>
<button id="shuffle" type="button" class="ghost">Shuffle target</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; }
.stage { display: flex; gap: 1rem; flex-wrap: wrap; }
.panel { background: #f3f6f9; border: 1px solid #d8e0e8; border-radius: 10px; padding: .5rem; }
.cap { font: 700 13px system-ui; color: #1d3557; margin-bottom: .2rem; }
svg { display: block; }
.edge { stroke: #adb5bd; stroke-width: 3; }
.edge.lit { stroke: #0a7d33; stroke-width: 4; }
.node { fill: #c9ccd1; stroke: #8b929b; stroke-width: 2; }
.node.mapped { fill: #1d3557; stroke: #122440; }
.node.cur { fill: #e63946; stroke: #c92f3c; }
.nlabel { font: 700 13px ui-monospace, monospace; fill: #fff; text-anchor: middle; dominant-baseline: central; }
.nlabel.dark { fill: #1d3557; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 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; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .5; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Subgraph isomorphism: can the PATTERN be embedded in the TARGET so every
// pattern edge maps to a target edge? Checking a map is a quick edge scan;
// finding one is NP-complete. We use backtracking (Ullmann-style) and animate it.
// Pattern: a triangle 0-1-2 plus a tail 2-3 (a "kite handle").
const PN = 4;
const PE = [[0,1],[1,2],[2,0],[2,3]];
const PPOS = [[40,40],[120,40],[80,100],[80,150]];
// Target: 6 vertices. Layout fixed; adjacency reshuffled to vary difficulty.
const TN = 6;
const TPOS = [[40,40],[120,30],[200,55],[60,120],[150,120],[210,150]];
let TE = []; // target edges
let adj = []; // adjacency matrix
function buildAdj(){
adj = Array.from({length:TN},()=>Array(TN).fill(false));
for(const [a,b] of TE){ adj[a][b]=true; adj[b][a]=true; }
}
const padj = Array.from({length:PN},()=>Array(PN).fill(false));
for(const [a,b] of PE){ padj[a][b]=true; padj[b][a]=true; }
function makeTarget(){
// Guarantee at least one embedding exists: pick 4 distinct target vertices
// as a planted copy of the pattern, then add a few random extra edges.
const perm = [...Array(TN).keys()];
for(let i=perm.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [perm[i],perm[j]]=[perm[j],perm[i]]; }
const set = new Set();
const add=(a,b)=>{ const k=a<b?a+'-'+b:b+'-'+a; set.add(k); };
for(const [a,b] of PE) add(perm[a], perm[b]); // planted pattern
// sprinkle random noise edges
let tries=0;
while(set.size < PE.length+3 && tries<40){ tries++;
const a=Math.floor(Math.random()*TN), b=Math.floor(Math.random()*TN);
if(a!==b) add(a,b);
}
TE = [...set].map(s=>s.split('-').map(Number));
buildAdj();
}
// ── Backtracking matcher. Returns {ok, map, steps}. map[p] = target vertex. ──
function solve(){
const map = Array(PN).fill(-1);
const used = Array(TN).fill(false);
let steps = 0;
function consistent(p, t){
for(let q=0;q<p;q++){ if(padj[p][q] && !adj[t][map[q]]) return false; }
return true;
}
function bt(p){
if(p===PN) return true;
for(let t=0;t<TN;t++){ steps++;
if(used[t]) continue;
if(!consistent(p,t)) continue;
map[p]=t; used[t]=true;
if(bt(p+1)) return true;
map[p]=-1; used[t]=false;
}
return false;
}
const ok = bt(0);
return { ok, map: ok?map.slice():null, steps };
}
// ── Rendering ──
function drawGraph(svg, n, edges, pos, mapInfo){
svg.innerHTML='';
const NS='http://www.w3.org/2000/svg';
for(const [a,b] of edges){
const l=document.createElementNS(NS,'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'+(mapInfo&&mapInfo.litEdges&&mapInfo.litEdges.has((a<b?a+'-'+b:b+'-'+a))?' lit':''));
svg.appendChild(l);
}
for(let i=0;i<n;i++){
const c=document.createElementNS(NS,'circle');
c.setAttribute('cx',pos[i][0]); c.setAttribute('cy',pos[i][1]); c.setAttribute('r',16);
let cls='node';
if(mapInfo){ if(mapInfo.cur===i) cls+=' cur'; else if(mapInfo.mapped&&mapInfo.mapped.has(i)) cls+=' mapped'; }
c.setAttribute('class',cls);
svg.appendChild(c);
const t=document.createElementNS(NS,'text');
t.setAttribute('x',pos[i][0]); t.setAttribute('y',pos[i][1]);
t.setAttribute('class','nlabel'+(cls==='node'?' dark':''));
t.textContent=String.fromCharCode((svg.id==='pat'?80:65)+i); // P:A.. / T:A..
svg.appendChild(t);
}
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
function renderStatic(){
drawGraph(document.getElementById('pat'), PN, PE, PPOS, null);
drawGraph(document.getElementById('tgt'), TN, TE, TPOS, null);
}
function animateResult(res){
const btn=document.getElementById('search'); btn.disabled=true;
if(!res.ok){
setStatus(fmt('No embedding exists (checked %steps% assignments).', { steps: res.steps }),'bad');
btn.disabled=false; return;
}
const mappedT=new Set(), mappedP=new Set();
let i=0;
const timer=setInterval(()=>{
if(i>=PN){ clearInterval(timer);
// light up matched edges on both graphs
const lit=new Set();
for(const [a,b] of PE){ const ta=res.map[a], tb=res.map[b]; lit.add(ta<tb?ta+'-'+tb:tb+'-'+ta); }
drawGraph(document.getElementById('tgt'), TN, TE, TPOS, {mapped:mappedT, litEdges:lit});
const litP=new Set(); for(const [a,b] of PE) litP.add(a<b?a+'-'+b:b+'-'+a);
drawGraph(document.getElementById('pat'), PN, PE, PPOS, {mapped:mappedP, litEdges:litP});
setStatus(fmt('Found! Pattern embeds via %map% (%steps% steps).', { map: res.map.map((t,p)=>String.fromCharCode(80+p)+'→'+String.fromCharCode(65+t)).join(', '), steps: res.steps }),'ok');
console.log('mapping', res.map, 'steps', res.steps);
btn.disabled=false; return;
}
const t=res.map[i];
drawGraph(document.getElementById('pat'), PN, PE, PPOS, {cur:i, mapped:new Set(mappedP)});
drawGraph(document.getElementById('tgt'), TN, TE, TPOS, {cur:t, mapped:new Set(mappedT)});
setStatus(fmt('Mapping %fromCharCode% → %fromCharCode2% …', { fromCharCode: String.fromCharCode(80+i), fromCharCode2: String.fromCharCode(65+t) }),'');
mappedP.add(i); mappedT.add(t); i++;
}, 650);
}
document.getElementById('search').addEventListener('click',()=>animateResult(solve()));
document.getElementById('shuffle').addEventListener('click',()=>{ makeTarget(); renderStatic(); setStatus('Target reshuffled. Press Search.',''); });
makeTarget(); renderStatic();
Notice the asymmetry. Verifying a proposed mapping is instant: check each pattern edge and confirm the matching target edge exists. Finding the mapping is the hard part — the matcher may try many dead ends before it succeeds or proves no embedding exists. Shuffle the target a few times and watch the number of backtracking steps swing wildly.
Comments
Loading comments...