Try it. The two graphs below are drawn very differently — but they are the same graph relabeled. Click a node on the left, then a node on the right to pair them. Build a full mapping, then Check whether every edge lines up.
<p class="hint">These two graphs are the <b>same graph relabeled</b>. Click a node on the <b>left</b>, then its match on the <b>right</b>, to pair them. Build a full mapping, then <b>Check</b> whether every edge lines up.</p>
<div class="boards">
<div><div class="cap">Graph A</div><svg id="ga" viewBox="0 0 180 180"></svg></div>
<div><div class="cap">Graph B</div><svg id="gb" viewBox="0 0 180 180"></svg></div>
</div>
<div id="map" class="maprow"></div>
<div id="status" class="status"></div>
<div class="btns">
<button id="check" type="button">Check</button>
<button id="solve" type="button">Auto-solve</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 .7rem; line-height: 1.45; }
.boards { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; }
.cap { font: 700 13px system-ui; color: #1d3557; text-align: center; margin-bottom: .2rem; }
svg { width: 100%; background: #f4f7f9; border: 1px solid #e2e6eb; border-radius: 10px; display: block; }
.edge { stroke: #b8c4cf; stroke-width: 2.5; }
.node { fill: #8aa0b3; cursor: pointer; transition: fill .1s; }
.node:hover { fill: #457b9d; }
.node.sel { fill: #e76f51; }
.node.mapped { fill: #2a9d8f; }
.nlbl { font: 700 12px ui-monospace, monospace; fill: #fff; pointer-events: none; text-anchor: middle; dominant-baseline: central; }
.maprow { display: flex; flex-wrap: wrap; gap: .35rem; margin: .7rem 0 .4rem; min-height: 1.6rem; }
.pair { font: 700 12px ui-monospace, monospace; background: #e6f6ec; color: #0a7d33; border: 1px solid #b6e3c5; border-radius: 6px; padding: .15rem .45rem; }
.status { font-weight: 800; min-height: 1.3em; margin-bottom: .5rem; }
.status.ok { color: #0a7d33; } .status.bad { color: #c0392b; }
.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] ?? ''));
// Graph isomorphism: are two graphs the same after relabeling? The user
// builds a node mapping A->B; Check verifies every A-edge maps to a B-edge.
// Auto-solve brute-forces the 5! = 120 permutations.
const N = 5;
// Graph A: 5-cycle + one chord
const EA = [[0, 1], [1, 2], [2, 3], [3, 4], [4, 0], [0, 2]];
// B is A relabeled by P (A-node i becomes B-node P[i]) and redrawn
const P = [2, 0, 3, 4, 1];
const EB = EA.map(([u, v]) => [P[u], P[v]]);
const PA = pentagon(90, 90, 65);
const PB = [[40, 40], [150, 60], [90, 160], [30, 130], [160, 150]]; // scattered
const adjB = new Set(EB.map(([u, v]) => key(u, v)));
function pentagon(cx, cy, r) { const p = []; for (let i = 0; i < N; i++) { const a = -Math.PI / 2 + i * 2 * Math.PI / N; p.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]); } return p; }
function key(u, v) { return u < v ? u + '-' + v : v + '-' + u; }
let map = {}; // A-node -> B-node
let selA = -1;
function drawGraph(svgId, pos, edges, side) {
let s = '';
edges.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 (side === 'A') { if (selA === i) cls += ' sel'; else if (map[i] !== undefined) cls += ' mapped'; }
else { if (Object.values(map).includes(i)) cls += ' mapped'; }
const label = side === 'A' ? 'a' + i : 'b' + i;
s += '<circle class="' + cls + '" cx="' + pos[i][0] + '" cy="' + pos[i][1] + '" r="15" data-i="' + i + '"/>';
s += '<text class="nlbl" x="' + pos[i][0] + '" y="' + pos[i][1] + '">' + label + '</text>';
}
const svg = document.getElementById(svgId);
svg.innerHTML = s;
svg.querySelectorAll('.node').forEach(n => n.addEventListener('click', () => click(side, +n.dataset.i)));
}
function click(side, i) {
if (side === 'A') { selA = (selA === i ? -1 : i); }
else if (selA >= 0) {
// remove any existing A-node pointing to this B-node
for (const k in map) if (map[k] === i) delete map[k];
map[selA] = i; selA = -1;
}
render();
}
function render() {
drawGraph('ga', PA, EA, 'A'); drawGraph('gb', PB, EB, 'B');
document.getElementById('map').innerHTML = Object.keys(map).sort()
.map(a => '<span class="pair">a' + a + ' → b' + map[a] + '</span>').join('');
}
function check() {
const st = document.getElementById('status');
if (Object.keys(map).length < N || new Set(Object.values(map)).size < N) {
st.className = 'status bad'; st.textContent = 'Map all five nodes one-to-one first.'; return;
}
const bad = EA.find(([u, v]) => !adjB.has(key(map[u], map[v])));
if (bad) { st.className = 'status bad'; st.textContent = fmt('✗ Edge a%v%–a%v2% has no match. Not an isomorphism — yet.', { v: bad[0], v2: bad[1] }); }
else { st.className = 'status ok'; st.textContent = '✓ Every edge lines up — the graphs are isomorphic!'; }
}
function* perms(arr) { if (arr.length <= 1) { yield arr; return; } for (let i = 0; i < arr.length; i++) { const rest = arr.slice(0, i).concat(arr.slice(i + 1)); for (const p of perms(rest)) yield [arr[i], ...p]; } }
function solve() {
for (const p of perms([0, 1, 2, 3, 4])) {
if (EA.every(([u, v]) => adjB.has(key(p[u], p[v])))) {
map = {}; p.forEach((b, a) => map[a] = b); selA = -1; render();
document.getElementById('status').className = 'status ok';
document.getElementById('status').textContent = '✓ Found a valid relabeling — the graphs are isomorphic.';
return;
}
}
}
document.getElementById('check').addEventListener('click', check);
document.getElementById('solve').addEventListener('click', solve);
document.getElementById('reset').addEventListener('click', () => { map = {}; selA = -1; document.getElementById('status').textContent = ''; render(); });
render();
<p class="hint">These two graphs are the <b>same graph relabeled</b>. Click a node on the <b>left</b>, then its match on the <b>right</b>, to pair them. Build a full mapping, then <b>Check</b> whether every edge lines up.</p>
<div class="boards">
<div><div class="cap">Graph A</div><svg id="ga" viewBox="0 0 180 180"></svg></div>
<div><div class="cap">Graph B</div><svg id="gb" viewBox="0 0 180 180"></svg></div>
</div>
<div id="map" class="maprow"></div>
<div id="status" class="status"></div>
<div class="btns">
<button id="check" type="button">Check</button>
<button id="solve" type="button">Auto-solve</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 .7rem; line-height: 1.45; }
.boards { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; }
.cap { font: 700 13px system-ui; color: #1d3557; text-align: center; margin-bottom: .2rem; }
svg { width: 100%; background: #f4f7f9; border: 1px solid #e2e6eb; border-radius: 10px; display: block; }
.edge { stroke: #b8c4cf; stroke-width: 2.5; }
.node { fill: #8aa0b3; cursor: pointer; transition: fill .1s; }
.node:hover { fill: #457b9d; }
.node.sel { fill: #e76f51; }
.node.mapped { fill: #2a9d8f; }
.nlbl { font: 700 12px ui-monospace, monospace; fill: #fff; pointer-events: none; text-anchor: middle; dominant-baseline: central; }
.maprow { display: flex; flex-wrap: wrap; gap: .35rem; margin: .7rem 0 .4rem; min-height: 1.6rem; }
.pair { font: 700 12px ui-monospace, monospace; background: #e6f6ec; color: #0a7d33; border: 1px solid #b6e3c5; border-radius: 6px; padding: .15rem .45rem; }
.status { font-weight: 800; min-height: 1.3em; margin-bottom: .5rem; }
.status.ok { color: #0a7d33; } .status.bad { color: #c0392b; }
.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] ?? ''));
// Graph isomorphism: are two graphs the same after relabeling? The user
// builds a node mapping A->B; Check verifies every A-edge maps to a B-edge.
// Auto-solve brute-forces the 5! = 120 permutations.
const N = 5;
// Graph A: 5-cycle + one chord
const EA = [[0, 1], [1, 2], [2, 3], [3, 4], [4, 0], [0, 2]];
// B is A relabeled by P (A-node i becomes B-node P[i]) and redrawn
const P = [2, 0, 3, 4, 1];
const EB = EA.map(([u, v]) => [P[u], P[v]]);
const PA = pentagon(90, 90, 65);
const PB = [[40, 40], [150, 60], [90, 160], [30, 130], [160, 150]]; // scattered
const adjB = new Set(EB.map(([u, v]) => key(u, v)));
function pentagon(cx, cy, r) { const p = []; for (let i = 0; i < N; i++) { const a = -Math.PI / 2 + i * 2 * Math.PI / N; p.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]); } return p; }
function key(u, v) { return u < v ? u + '-' + v : v + '-' + u; }
let map = {}; // A-node -> B-node
let selA = -1;
function drawGraph(svgId, pos, edges, side) {
let s = '';
edges.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 (side === 'A') { if (selA === i) cls += ' sel'; else if (map[i] !== undefined) cls += ' mapped'; }
else { if (Object.values(map).includes(i)) cls += ' mapped'; }
const label = side === 'A' ? 'a' + i : 'b' + i;
s += '<circle class="' + cls + '" cx="' + pos[i][0] + '" cy="' + pos[i][1] + '" r="15" data-i="' + i + '"/>';
s += '<text class="nlbl" x="' + pos[i][0] + '" y="' + pos[i][1] + '">' + label + '</text>';
}
const svg = document.getElementById(svgId);
svg.innerHTML = s;
svg.querySelectorAll('.node').forEach(n => n.addEventListener('click', () => click(side, +n.dataset.i)));
}
function click(side, i) {
if (side === 'A') { selA = (selA === i ? -1 : i); }
else if (selA >= 0) {
// remove any existing A-node pointing to this B-node
for (const k in map) if (map[k] === i) delete map[k];
map[selA] = i; selA = -1;
}
render();
}
function render() {
drawGraph('ga', PA, EA, 'A'); drawGraph('gb', PB, EB, 'B');
document.getElementById('map').innerHTML = Object.keys(map).sort()
.map(a => '<span class="pair">a' + a + ' → b' + map[a] + '</span>').join('');
}
function check() {
const st = document.getElementById('status');
if (Object.keys(map).length < N || new Set(Object.values(map)).size < N) {
st.className = 'status bad'; st.textContent = 'Map all five nodes one-to-one first.'; return;
}
const bad = EA.find(([u, v]) => !adjB.has(key(map[u], map[v])));
if (bad) { st.className = 'status bad'; st.textContent = fmt('✗ Edge a%v%–a%v2% has no match. Not an isomorphism — yet.', { v: bad[0], v2: bad[1] }); }
else { st.className = 'status ok'; st.textContent = '✓ Every edge lines up — the graphs are isomorphic!'; }
}
function* perms(arr) { if (arr.length <= 1) { yield arr; return; } for (let i = 0; i < arr.length; i++) { const rest = arr.slice(0, i).concat(arr.slice(i + 1)); for (const p of perms(rest)) yield [arr[i], ...p]; } }
function solve() {
for (const p of perms([0, 1, 2, 3, 4])) {
if (EA.every(([u, v]) => adjB.has(key(p[u], p[v])))) {
map = {}; p.forEach((b, a) => map[a] = b); selA = -1; render();
document.getElementById('status').className = 'status ok';
document.getElementById('status').textContent = '✓ Found a valid relabeling — the graphs are isomorphic.';
return;
}
}
}
document.getElementById('check').addEventListener('click', check);
document.getElementById('solve').addEventListener('click', solve);
document.getElementById('reset').addEventListener('click', () => { map = {}; selA = -1; document.getElementById('status').textContent = ''; render(); });
render();
Get one wrong and an edge won't match. Stuck? Auto-solve searches the relabelings for you. With just five nodes that's quick — but imagine the search over hundreds of nodes, and you feel why this needs cleverness, not brute force.
Comments
Loading comments...