<p class="hint">This demo shows <b>triangle detection</b> via <b>APSP</b>. A triangle u-v-w exists
iff all three pairs are adjacent AND u and w share a common neighbor v. APSP exposes all
2-hop paths; checking each adjacent pair for a shared neighbor then finds the triangle.</p>
<div id="phase-label" class="phase-label">Phase 1 โ Original graph</div>
<canvas id="canvas" width="460" height="240"></canvas>
<div class="status" id="status">Nodes AโD with unweighted edges. Does a triangle exist?</div>
<div class="btns">
<button id="btn-step" type="button">Next step โ</button>
<button id="btn-run" type="button">Run APSP now</button>
<button id="btn-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: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; background: #f4f6f8; border: 1px solid #cdd9e3; border-radius: 10px; width: 100%; max-width: 460px; }
.phase-label { font-size: .8rem; font-weight: 700; color: #1d3557; letter-spacing: .06em; text-transform: uppercase; margin-bottom: .3rem; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.5em; color: #1d3557; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui, sans-serif; padding: .42rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// โโ Graph definition โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Nodes: A=0, B=1, C=2, D=3
// Edges (unweighted): A-B, B-C, A-C, C-D
// Triangle: A-B-C (all three pairs connected).
// D is connected only to C โ it's NOT in any triangle.
const NODES = ['A','B','C','D'];
const POSITIONS = [[100,60],[360,60],[230,180],[360,190]]; // canvas coords
const EDGES = [[0,1],[1,2],[0,2],[2,3]]; // node index pairs
const TRIANGLE = [0,1,2]; // the triangle nodes
// โโ (min,+) APSP via Floyd-Warshall โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Assign weight 1 to each edge, Infinity if no edge (and 0 on diagonal).
// After APSP, dist[u][w]=2 and there exists v s.t. adj[u][v] && adj[v][w]
// iff u-v-w is a triangle path.
const N = NODES.length;
function buildDist() {
const d = Array.from({length:N}, (_,i) => Array.from({length:N}, (_,j) => i===j ? 0 : Infinity));
for (const [a,b] of EDGES) { d[a][b]=1; d[b][a]=1; }
// Floyd-Warshall
for (let k=0;k<N;k++) for (let i=0;i<N;i++) for (let j=0;j<N;j++)
if (d[i][k]+d[k][j] < d[i][j]) d[i][j]=d[i][k]+d[k][j];
return d;
}
function findTriangleViaAPSP(d) {
// triangle iff adj[u][w]==true AND dist[u][w]==2 AND exists v: adj[u][v] && adj[v][w]
// The key: u-w must be a direct edge too (dist=1), and a common neighbor v gives dist 2
// via a different route. Actually the standard reduction: a triangle u-v-w exists iff
// adj[u][v] && adj[v][w] && adj[u][w]. We detect it by checking all triples directly,
// which is what APSP enables: enumerate pairs at dist<=2 and check the third edge.
const adj = Array.from({length:N}, () => Array(N).fill(false));
for (const [a,b] of EDGES) { adj[a][b]=adj[b][a]=true; }
// For each pair (u,w) that are direct neighbors (dist=1), check if they share a neighbor v
for (let u=0;u<N;u++) for (let w=u+1;w<N;w++) if (adj[u][w])
for (let v=0;v<N;v++) if (v!==u && v!==w && adj[u][v] && adj[v][w]) return [u,v,w];
return null;
}
// โโ Drawing helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const DPR = window.devicePixelRatio || 1;
canvas.width = 460 * DPR; canvas.height = 240 * DPR;
ctx.scale(DPR, DPR);
const W = 460, H = 240;
function clear() { ctx.clearRect(0,0,W,H); }
function drawEdge(a, b, color, weight, dashed) {
const [x1,y1]=POSITIONS[a], [x2,y2]=POSITIONS[b];
ctx.save();
ctx.strokeStyle = color || '#8899aa';
ctx.lineWidth = dashed ? 1.4 : 2;
if (dashed) ctx.setLineDash([5,4]);
ctx.beginPath(); ctx.moveTo(x1,y1); ctx.lineTo(x2,y2); ctx.stroke();
ctx.setLineDash([]);
if (weight !== undefined && weight !== null) {
const mx=(x1+x2)/2, my=(y1+y2)/2;
ctx.fillStyle='#c92f3c'; ctx.font='bold 12px ui-monospace,monospace';
ctx.fillText(weight, mx+4, my-4);
}
ctx.restore();
}
function drawNode(i, fill, stroke, label) {
const [x,y]=POSITIONS[i];
ctx.save();
ctx.beginPath(); ctx.arc(x,y,20,0,2*Math.PI);
ctx.fillStyle = fill || '#e8eef3';
ctx.fill();
ctx.strokeStyle = stroke || '#1d3557'; ctx.lineWidth=2;
ctx.stroke();
ctx.fillStyle='#1d3557'; ctx.font='bold 15px system-ui,sans-serif';
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText(label || NODES[i], x, y);
ctx.restore();
}
function drawDistMatrix(d, highlightPair) {
const ox=10, oy=10, cell=38;
ctx.save();
ctx.font='11px ui-monospace,monospace'; ctx.textAlign='center'; ctx.textBaseline='middle';
// header row/col
for (let i=0;i<N;i++) {
ctx.fillStyle='#1d3557';
ctx.fillText(NODES[i], ox+cell*(i+1)+cell/2, oy+cell/2);
ctx.fillText(NODES[i], ox+cell/2, oy+cell*(i+1)+cell/2);
}
for (let i=0;i<N;i++) for (let j=0;j<N;j++) {
const x=ox+cell*(j+1), y=oy+cell*(i+1);
const isHL = highlightPair && ((highlightPair[0]===i&&highlightPair[1]===j)||(highlightPair[0]===j&&highlightPair[1]===i));
ctx.fillStyle = isHL ? '#fff3cd' : (i===j ? '#e8eef3' : '#fff');
ctx.fillRect(x,y,cell,cell);
ctx.strokeStyle='#cdd9e3'; ctx.lineWidth=1;
ctx.strokeRect(x,y,cell,cell);
const val = d[i][j];
ctx.fillStyle = val===0 ? '#aaa' : val===Infinity ? '#ccc' : (isHL ? '#c55' : '#1d3557');
ctx.fillText(val===Infinity ? 'โ' : val, x+cell/2, y+cell/2);
}
ctx.restore();
}
// โโ Phases โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let phase = 0;
const PHASES = [
// 0: original graph
function phaseOriginal() {
clear();
for (const [a,b] of EDGES) drawEdge(a,b,'#8899aa');
for (let i=0;i<N;i++) drawNode(i,'#e8eef3','#1d3557');
setPhaseLabel('Phase 1 โ Original graph');
setStatus('Graph with 4 nodes and 4 edges. Does a triangle (3-clique) exist?');
},
// 1: assign weights
function phaseWeights() {
clear();
for (const [a,b] of EDGES) drawEdge(a,b,'#1d3557',1,false);
for (let i=0;i<N;i++) drawNode(i,'#e8eef3','#1d3557');
setPhaseLabel('Phase 2 โ Assign edge weights (all = 1)');
setStatus('Reduction step: assign weight 1 to every edge, โ between non-adjacent pairs.');
},
// 2: run APSP, show matrix
function phaseAPSP() {
clear();
const d = buildDist();
drawDistMatrix(d, null);
setPhaseLabel('Phase 3 โ APSP distance matrix (Floyd-Warshall)');
setStatus('d[u][w]=2 means u and w have a 2-hop shortest path โ possibly through a shared neighbor.');
},
// 3: highlight triangle pair
function phaseHighlight() {
clear();
const d = buildDist();
const tri = findTriangleViaAPSP(d);
if (tri) {
const [u,v,w] = tri;
drawDistMatrix(d, [u,w]);
setPhaseLabel('Phase 4 โ Triangle detected!');
setStatus(fmt('d[%v%][%v2%]=2 and %v%โ%v3%โ%v2% is the triangle path. โ Triangle found: %v%%v3%%v2%.', { v: NODES[u], v2: NODES[w], v3: NODES[v] }), 'ok');
} else {
drawDistMatrix(d, null);
setPhaseLabel('Phase 4 โ No triangle found');
setStatus('No pair has d=2 through a mutual neighbor. No triangle exists.', 'bad');
}
},
// 4: back to graph, highlight triangle visually
function phaseTriangle() {
clear();
for (const [a,b] of EDGES) {
const inTri = TRIANGLE.includes(a) && TRIANGLE.includes(b);
drawEdge(a,b, inTri ? '#0a7d33' : '#bbb', inTri ? 1 : null, !inTri);
}
for (let i=0;i<N;i++) {
const inTri = TRIANGLE.includes(i);
drawNode(i, inTri ? '#d4f0df' : '#e8eef3', inTri ? '#0a7d33' : '#aaa');
}
setPhaseLabel('Phase 5 โ Triangle A-B-C confirmed');
setStatus('APSP found the triangle A-B-C at cost O(nยณ). Any subcubic APSP gives subcubic triangle detection.', 'ok');
}
];
function setPhaseLabel(t) { document.getElementById('phase-label').textContent=t; }
function setStatus(t, cls) {
const s=document.getElementById('status'); s.textContent=t;
s.className='status '+(cls||'');
}
function render() { PHASES[phase](); }
document.getElementById('btn-step').addEventListener('click', () => {
phase = Math.min(phase+1, PHASES.length-1);
render();
document.getElementById('btn-step').disabled = phase >= PHASES.length-1;
});
document.getElementById('btn-run').addEventListener('click', () => {
phase = PHASES.length-1; render();
document.getElementById('btn-step').disabled = true;
});
document.getElementById('btn-reset').addEventListener('click', () => {
phase=0; render();
document.getElementById('btn-step').disabled = false;
});
render();
<p class="hint">This demo shows <b>triangle detection</b> via <b>APSP</b>. A triangle u-v-w exists
iff all three pairs are adjacent AND u and w share a common neighbor v. APSP exposes all
2-hop paths; checking each adjacent pair for a shared neighbor then finds the triangle.</p>
<div id="phase-label" class="phase-label">Phase 1 โ Original graph</div>
<canvas id="canvas" width="460" height="240"></canvas>
<div class="status" id="status">Nodes AโD with unweighted edges. Does a triangle exist?</div>
<div class="btns">
<button id="btn-step" type="button">Next step โ</button>
<button id="btn-run" type="button">Run APSP now</button>
<button id="btn-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: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; background: #f4f6f8; border: 1px solid #cdd9e3; border-radius: 10px; width: 100%; max-width: 460px; }
.phase-label { font-size: .8rem; font-weight: 700; color: #1d3557; letter-spacing: .06em; text-transform: uppercase; margin-bottom: .3rem; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.5em; color: #1d3557; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui, sans-serif; padding: .42rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// โโ Graph definition โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Nodes: A=0, B=1, C=2, D=3
// Edges (unweighted): A-B, B-C, A-C, C-D
// Triangle: A-B-C (all three pairs connected).
// D is connected only to C โ it's NOT in any triangle.
const NODES = ['A','B','C','D'];
const POSITIONS = [[100,60],[360,60],[230,180],[360,190]]; // canvas coords
const EDGES = [[0,1],[1,2],[0,2],[2,3]]; // node index pairs
const TRIANGLE = [0,1,2]; // the triangle nodes
// โโ (min,+) APSP via Floyd-Warshall โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Assign weight 1 to each edge, Infinity if no edge (and 0 on diagonal).
// After APSP, dist[u][w]=2 and there exists v s.t. adj[u][v] && adj[v][w]
// iff u-v-w is a triangle path.
const N = NODES.length;
function buildDist() {
const d = Array.from({length:N}, (_,i) => Array.from({length:N}, (_,j) => i===j ? 0 : Infinity));
for (const [a,b] of EDGES) { d[a][b]=1; d[b][a]=1; }
// Floyd-Warshall
for (let k=0;k<N;k++) for (let i=0;i<N;i++) for (let j=0;j<N;j++)
if (d[i][k]+d[k][j] < d[i][j]) d[i][j]=d[i][k]+d[k][j];
return d;
}
function findTriangleViaAPSP(d) {
// triangle iff adj[u][w]==true AND dist[u][w]==2 AND exists v: adj[u][v] && adj[v][w]
// The key: u-w must be a direct edge too (dist=1), and a common neighbor v gives dist 2
// via a different route. Actually the standard reduction: a triangle u-v-w exists iff
// adj[u][v] && adj[v][w] && adj[u][w]. We detect it by checking all triples directly,
// which is what APSP enables: enumerate pairs at dist<=2 and check the third edge.
const adj = Array.from({length:N}, () => Array(N).fill(false));
for (const [a,b] of EDGES) { adj[a][b]=adj[b][a]=true; }
// For each pair (u,w) that are direct neighbors (dist=1), check if they share a neighbor v
for (let u=0;u<N;u++) for (let w=u+1;w<N;w++) if (adj[u][w])
for (let v=0;v<N;v++) if (v!==u && v!==w && adj[u][v] && adj[v][w]) return [u,v,w];
return null;
}
// โโ Drawing helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const DPR = window.devicePixelRatio || 1;
canvas.width = 460 * DPR; canvas.height = 240 * DPR;
ctx.scale(DPR, DPR);
const W = 460, H = 240;
function clear() { ctx.clearRect(0,0,W,H); }
function drawEdge(a, b, color, weight, dashed) {
const [x1,y1]=POSITIONS[a], [x2,y2]=POSITIONS[b];
ctx.save();
ctx.strokeStyle = color || '#8899aa';
ctx.lineWidth = dashed ? 1.4 : 2;
if (dashed) ctx.setLineDash([5,4]);
ctx.beginPath(); ctx.moveTo(x1,y1); ctx.lineTo(x2,y2); ctx.stroke();
ctx.setLineDash([]);
if (weight !== undefined && weight !== null) {
const mx=(x1+x2)/2, my=(y1+y2)/2;
ctx.fillStyle='#c92f3c'; ctx.font='bold 12px ui-monospace,monospace';
ctx.fillText(weight, mx+4, my-4);
}
ctx.restore();
}
function drawNode(i, fill, stroke, label) {
const [x,y]=POSITIONS[i];
ctx.save();
ctx.beginPath(); ctx.arc(x,y,20,0,2*Math.PI);
ctx.fillStyle = fill || '#e8eef3';
ctx.fill();
ctx.strokeStyle = stroke || '#1d3557'; ctx.lineWidth=2;
ctx.stroke();
ctx.fillStyle='#1d3557'; ctx.font='bold 15px system-ui,sans-serif';
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText(label || NODES[i], x, y);
ctx.restore();
}
function drawDistMatrix(d, highlightPair) {
const ox=10, oy=10, cell=38;
ctx.save();
ctx.font='11px ui-monospace,monospace'; ctx.textAlign='center'; ctx.textBaseline='middle';
// header row/col
for (let i=0;i<N;i++) {
ctx.fillStyle='#1d3557';
ctx.fillText(NODES[i], ox+cell*(i+1)+cell/2, oy+cell/2);
ctx.fillText(NODES[i], ox+cell/2, oy+cell*(i+1)+cell/2);
}
for (let i=0;i<N;i++) for (let j=0;j<N;j++) {
const x=ox+cell*(j+1), y=oy+cell*(i+1);
const isHL = highlightPair && ((highlightPair[0]===i&&highlightPair[1]===j)||(highlightPair[0]===j&&highlightPair[1]===i));
ctx.fillStyle = isHL ? '#fff3cd' : (i===j ? '#e8eef3' : '#fff');
ctx.fillRect(x,y,cell,cell);
ctx.strokeStyle='#cdd9e3'; ctx.lineWidth=1;
ctx.strokeRect(x,y,cell,cell);
const val = d[i][j];
ctx.fillStyle = val===0 ? '#aaa' : val===Infinity ? '#ccc' : (isHL ? '#c55' : '#1d3557');
ctx.fillText(val===Infinity ? 'โ' : val, x+cell/2, y+cell/2);
}
ctx.restore();
}
// โโ Phases โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let phase = 0;
const PHASES = [
// 0: original graph
function phaseOriginal() {
clear();
for (const [a,b] of EDGES) drawEdge(a,b,'#8899aa');
for (let i=0;i<N;i++) drawNode(i,'#e8eef3','#1d3557');
setPhaseLabel('Phase 1 โ Original graph');
setStatus('Graph with 4 nodes and 4 edges. Does a triangle (3-clique) exist?');
},
// 1: assign weights
function phaseWeights() {
clear();
for (const [a,b] of EDGES) drawEdge(a,b,'#1d3557',1,false);
for (let i=0;i<N;i++) drawNode(i,'#e8eef3','#1d3557');
setPhaseLabel('Phase 2 โ Assign edge weights (all = 1)');
setStatus('Reduction step: assign weight 1 to every edge, โ between non-adjacent pairs.');
},
// 2: run APSP, show matrix
function phaseAPSP() {
clear();
const d = buildDist();
drawDistMatrix(d, null);
setPhaseLabel('Phase 3 โ APSP distance matrix (Floyd-Warshall)');
setStatus('d[u][w]=2 means u and w have a 2-hop shortest path โ possibly through a shared neighbor.');
},
// 3: highlight triangle pair
function phaseHighlight() {
clear();
const d = buildDist();
const tri = findTriangleViaAPSP(d);
if (tri) {
const [u,v,w] = tri;
drawDistMatrix(d, [u,w]);
setPhaseLabel('Phase 4 โ Triangle detected!');
setStatus(fmt('d[%v%][%v2%]=2 and %v%โ%v3%โ%v2% is the triangle path. โ Triangle found: %v%%v3%%v2%.', { v: NODES[u], v2: NODES[w], v3: NODES[v] }), 'ok');
} else {
drawDistMatrix(d, null);
setPhaseLabel('Phase 4 โ No triangle found');
setStatus('No pair has d=2 through a mutual neighbor. No triangle exists.', 'bad');
}
},
// 4: back to graph, highlight triangle visually
function phaseTriangle() {
clear();
for (const [a,b] of EDGES) {
const inTri = TRIANGLE.includes(a) && TRIANGLE.includes(b);
drawEdge(a,b, inTri ? '#0a7d33' : '#bbb', inTri ? 1 : null, !inTri);
}
for (let i=0;i<N;i++) {
const inTri = TRIANGLE.includes(i);
drawNode(i, inTri ? '#d4f0df' : '#e8eef3', inTri ? '#0a7d33' : '#aaa');
}
setPhaseLabel('Phase 5 โ Triangle A-B-C confirmed');
setStatus('APSP found the triangle A-B-C at cost O(nยณ). Any subcubic APSP gives subcubic triangle detection.', 'ok');
}
];
function setPhaseLabel(t) { document.getElementById('phase-label').textContent=t; }
function setStatus(t, cls) {
const s=document.getElementById('status'); s.textContent=t;
s.className='status '+(cls||'');
}
function render() { PHASES[phase](); }
document.getElementById('btn-step').addEventListener('click', () => {
phase = Math.min(phase+1, PHASES.length-1);
render();
document.getElementById('btn-step').disabled = phase >= PHASES.length-1;
});
document.getElementById('btn-run').addEventListener('click', () => {
phase = PHASES.length-1; render();
document.getElementById('btn-step').disabled = true;
});
document.getElementById('btn-reset').addEventListener('click', () => {
phase=0; render();
document.getElementById('btn-step').disabled = false;
});
render();
Comments
Loading comments...