The graph below has eight vertices and several edges. Click Find Bridges & Cut Vertices to run Tarjan's algorithm: bridges turn red and articulation points get a bold ring. You can also click Add Random Edge to make the graph more redundant and re-run to see bridges disappear.
<p class="hint">An undirected graph with 8 vertices. Click <b>Find Bridges & Cut Vertices</b> to run Tarjan's algorithm — bridges glow red, articulation points get a bold ring. Use <b>Add Random Edge</b> to make the graph more redundant and re-run.</p>
<canvas id="gc" width="480" height="300"></canvas>
<div class="status" id="status">Press the button to analyse the graph.</div>
<div class="btns">
<button id="find" type="button">Find Bridges & Cut Vertices</button>
<button id="addedge" type="button">Add Random Edge</button>
<button id="reset" type="button" class="ghost">Reset Graph</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; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 10px; background: #f5f8fa;
max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.5em; }
.status.bridge { color: #c92f3c; }
.status.ok { color: #0a7d33; }
.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] ?? ''));
// Tarjan's bridge / articulation-point algorithm (1972), iterative.
// For each tree edge (p -> u), edge is a bridge iff low[u] > disc[p].
// Vertex p is an articulation point iff:
// (a) p is the DFS root with >= 2 tree children, or
// (b) p is a non-root with a child u where low[u] >= disc[p].
// Key fix: track parent by EDGE INDEX (not vertex ID) so the algorithm works correctly even when two vertices share multiple paths.
const W = 480, H = 300, R = 14;
// Fixed vertex positions (0..1 normalised, scaled to canvas)
const POS = [
[0.12, 0.50], // 0 — left hub
[0.30, 0.18], // 1
[0.30, 0.82], // 2
[0.50, 0.50], // 3 — central articulation point
[0.70, 0.18], // 4
[0.70, 0.82], // 5
[0.88, 0.30], // 6
[0.88, 0.70], // 7
];
// Default edges: left triangle {0,1,2} connected to centre 3 via two edges;
// then 3-4, 3-5 (bridges), 4-6, 5-7 (bridges). Bridges: 3-4, 3-5, 4-6, 5-7. Articulation points: 3, 4, 5.
const DEFAULT_EDGES = [
[0,1],[0,2],[1,2], // left triangle — internally connected
[1,3],[2,3], // two paths from triangle to centre (3 is NOT a bridge endpoint here)
[3,4],[3,5], // bridges: only path from 3 to the right
[4,6],[5,7], // bridges: only path from 4 to 6, 5 to 7
];
let edges = DEFAULT_EDGES.map(e => [...e]);
let bridges = new Set(); // "a,b" with a < b
let artPoints = new Set(); // vertex indices
function px(i) { return Math.round(POS[i][0] * W); }
function py(i) { return Math.round(POS[i][1] * H); }
// ── Iterative Tarjan ────────────────────────────────────────────────────────
function tarjan(n, edgeList) {
// Build adjacency with {to, idx} so we track edges by index, not vertex.
const adj = Array.from({length: n}, () => []);
edgeList.forEach(([a, b], idx) => {
adj[a].push({ to: b, idx });
adj[b].push({ to: a, idx });
});
const disc = new Array(n).fill(-1);
const low = new Array(n).fill(0);
const par = new Array(n).fill(-1); // parent vertex
const parEdge = new Array(n).fill(-1); // parent edge index
const childCount = new Array(n).fill(0);
const newBridges = new Set();
const newAP = new Set();
let timer = 0;
for (let src = 0; src < n; src++) {
if (disc[src] !== -1) continue;
disc[src] = low[src] = timer++;
const stk = [[src, 0]]; // [vertex, adj-list cursor]
while (stk.length) {
const frame = stk[stk.length - 1];
const u = frame[0];
let pushed = false;
while (frame[1] < adj[u].length) {
const { to: v, idx } = adj[u][frame[1]];
frame[1]++;
if (idx === parEdge[u]) continue; // skip the edge we arrived on
if (disc[v] === -1) {
// Tree edge — recurse into v
disc[v] = low[v] = timer++;
par[v] = u;
parEdge[v] = idx;
childCount[u]++;
stk.push([v, 0]);
pushed = true;
break;
} else {
// Back edge — update low
low[u] = Math.min(low[u], disc[v]);
}
}
if (!pushed) {
// All neighbours of u exhausted — pop and propagate upward
stk.pop();
const p = par[u];
if (p !== -1) {
low[p] = Math.min(low[p], low[u]);
if (low[u] > disc[p]) {
newBridges.add(Math.min(p, u) + ',' + Math.max(p, u));
}
if (par[p] !== -1 && low[u] >= disc[p]) {
newAP.add(p); // non-root articulation point
}
}
}
}
}
// Root articulation points: DFS root with >= 2 tree children
for (let v = 0; v < n; v++) {
if (par[v] === -1 && disc[v] !== -1 && childCount[v] >= 2) newAP.add(v);
}
bridges = newBridges;
artPoints = newAP;
}
// ── Rendering ───────────────────────────────────────────────────────
function draw() {
const canvas = document.getElementById('gc');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, W, H);
edges.forEach(([a, b]) => {
const key = Math.min(a, b) + ',' + Math.max(a, b);
const isBridge = bridges.has(key);
ctx.beginPath();
ctx.moveTo(px(a), py(a));
ctx.lineTo(px(b), py(b));
ctx.strokeStyle = isBridge ? '#e63946' : '#7a9ab5';
ctx.lineWidth = isBridge ? 3.5 : 2;
ctx.stroke();
});
POS.forEach((_, i) => {
const x = px(i), y = py(i);
const isAP = artPoints.has(i);
if (isAP) {
ctx.beginPath();
ctx.arc(x, y, R + 5, 0, 2 * Math.PI);
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 3;
ctx.stroke();
}
ctx.beginPath();
ctx.arc(x, y, R, 0, 2 * Math.PI);
ctx.fillStyle = isAP ? '#fde8ea' : '#1d3557';
ctx.fill();
ctx.strokeStyle = isAP ? '#e63946' : '#1d3557';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = isAP ? '#c92f3c' : '#fff';
ctx.font = 'bold 12px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(i, x, y);
});
}
function setStatus(text, cls) {
const el = document.getElementById('status');
el.textContent = text;
el.className = 'status ' + (cls || '');
}
function analyse() {
tarjan(POS.length, edges);
draw();
const bCount = bridges.size;
const aCount = artPoints.size;
if (bCount === 0 && aCount === 0) {
setStatus('No bridges or articulation points — the graph is 2-connected!', 'ok');
} else {
const bList = bCount
? [...bridges].sort().map(k => '(' + k.replace(',', '–') + ')').join(', ')
: 'none';
const aList = aCount
? [...artPoints].sort((a, b) => a - b).join(', ')
: 'none';
setStatus(fmt('Bridges: %bList%. Articulation points: vertex %aList%.', { bList: bList, aList: aList }), 'bridge');
}
}
function addRandomEdge() {
const n = POS.length;
const existing = new Set(edges.map(([a, b]) => Math.min(a, b) + ',' + Math.max(a, b)));
const candidates = [];
for (let a = 0; a < n; a++)
for (let b = a + 1; b < n; b++)
if (!existing.has(a + ',' + b)) candidates.push([a, b]);
if (!candidates.length) {
setStatus('Graph is already complete — no more edges to add.', 'ok');
return;
}
const pick = candidates[Math.floor(Math.random() * candidates.length)];
edges.push(pick);
bridges = new Set(); artPoints = new Set();
draw();
setStatus(fmt('Added edge (%v%–%v2%). Press Find to re-analyse.', { v: pick[0], v2: pick[1] }));
}
function resetGraph() {
edges = DEFAULT_EDGES.map(e => [...e]);
bridges = new Set(); artPoints = new Set();
draw();
setStatus('Graph reset. Press the button to analyse.');
}
document.getElementById('find').addEventListener('click', analyse);
document.getElementById('addedge').addEventListener('click', addRandomEdge);
document.getElementById('reset').addEventListener('click', resetGraph);
draw(); // initial plain draw
console.log('Bridges & Articulation Points demo ready.');
console.log('Default bridges: 3-4, 3-5, 4-6, 5-7. Articulation points: 3, 4, 5.');
<p class="hint">An undirected graph with 8 vertices. Click <b>Find Bridges & Cut Vertices</b> to run Tarjan's algorithm — bridges glow red, articulation points get a bold ring. Use <b>Add Random Edge</b> to make the graph more redundant and re-run.</p>
<canvas id="gc" width="480" height="300"></canvas>
<div class="status" id="status">Press the button to analyse the graph.</div>
<div class="btns">
<button id="find" type="button">Find Bridges & Cut Vertices</button>
<button id="addedge" type="button">Add Random Edge</button>
<button id="reset" type="button" class="ghost">Reset Graph</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; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 10px; background: #f5f8fa;
max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.5em; }
.status.bridge { color: #c92f3c; }
.status.ok { color: #0a7d33; }
.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] ?? ''));
// Tarjan's bridge / articulation-point algorithm (1972), iterative.
// For each tree edge (p -> u), edge is a bridge iff low[u] > disc[p].
// Vertex p is an articulation point iff:
// (a) p is the DFS root with >= 2 tree children, or
// (b) p is a non-root with a child u where low[u] >= disc[p].
// Key fix: track parent by EDGE INDEX (not vertex ID) so the algorithm works correctly even when two vertices share multiple paths.
const W = 480, H = 300, R = 14;
// Fixed vertex positions (0..1 normalised, scaled to canvas)
const POS = [
[0.12, 0.50], // 0 — left hub
[0.30, 0.18], // 1
[0.30, 0.82], // 2
[0.50, 0.50], // 3 — central articulation point
[0.70, 0.18], // 4
[0.70, 0.82], // 5
[0.88, 0.30], // 6
[0.88, 0.70], // 7
];
// Default edges: left triangle {0,1,2} connected to centre 3 via two edges;
// then 3-4, 3-5 (bridges), 4-6, 5-7 (bridges). Bridges: 3-4, 3-5, 4-6, 5-7. Articulation points: 3, 4, 5.
const DEFAULT_EDGES = [
[0,1],[0,2],[1,2], // left triangle — internally connected
[1,3],[2,3], // two paths from triangle to centre (3 is NOT a bridge endpoint here)
[3,4],[3,5], // bridges: only path from 3 to the right
[4,6],[5,7], // bridges: only path from 4 to 6, 5 to 7
];
let edges = DEFAULT_EDGES.map(e => [...e]);
let bridges = new Set(); // "a,b" with a < b
let artPoints = new Set(); // vertex indices
function px(i) { return Math.round(POS[i][0] * W); }
function py(i) { return Math.round(POS[i][1] * H); }
// ── Iterative Tarjan ────────────────────────────────────────────────────────
function tarjan(n, edgeList) {
// Build adjacency with {to, idx} so we track edges by index, not vertex.
const adj = Array.from({length: n}, () => []);
edgeList.forEach(([a, b], idx) => {
adj[a].push({ to: b, idx });
adj[b].push({ to: a, idx });
});
const disc = new Array(n).fill(-1);
const low = new Array(n).fill(0);
const par = new Array(n).fill(-1); // parent vertex
const parEdge = new Array(n).fill(-1); // parent edge index
const childCount = new Array(n).fill(0);
const newBridges = new Set();
const newAP = new Set();
let timer = 0;
for (let src = 0; src < n; src++) {
if (disc[src] !== -1) continue;
disc[src] = low[src] = timer++;
const stk = [[src, 0]]; // [vertex, adj-list cursor]
while (stk.length) {
const frame = stk[stk.length - 1];
const u = frame[0];
let pushed = false;
while (frame[1] < adj[u].length) {
const { to: v, idx } = adj[u][frame[1]];
frame[1]++;
if (idx === parEdge[u]) continue; // skip the edge we arrived on
if (disc[v] === -1) {
// Tree edge — recurse into v
disc[v] = low[v] = timer++;
par[v] = u;
parEdge[v] = idx;
childCount[u]++;
stk.push([v, 0]);
pushed = true;
break;
} else {
// Back edge — update low
low[u] = Math.min(low[u], disc[v]);
}
}
if (!pushed) {
// All neighbours of u exhausted — pop and propagate upward
stk.pop();
const p = par[u];
if (p !== -1) {
low[p] = Math.min(low[p], low[u]);
if (low[u] > disc[p]) {
newBridges.add(Math.min(p, u) + ',' + Math.max(p, u));
}
if (par[p] !== -1 && low[u] >= disc[p]) {
newAP.add(p); // non-root articulation point
}
}
}
}
}
// Root articulation points: DFS root with >= 2 tree children
for (let v = 0; v < n; v++) {
if (par[v] === -1 && disc[v] !== -1 && childCount[v] >= 2) newAP.add(v);
}
bridges = newBridges;
artPoints = newAP;
}
// ── Rendering ───────────────────────────────────────────────────────
function draw() {
const canvas = document.getElementById('gc');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, W, H);
edges.forEach(([a, b]) => {
const key = Math.min(a, b) + ',' + Math.max(a, b);
const isBridge = bridges.has(key);
ctx.beginPath();
ctx.moveTo(px(a), py(a));
ctx.lineTo(px(b), py(b));
ctx.strokeStyle = isBridge ? '#e63946' : '#7a9ab5';
ctx.lineWidth = isBridge ? 3.5 : 2;
ctx.stroke();
});
POS.forEach((_, i) => {
const x = px(i), y = py(i);
const isAP = artPoints.has(i);
if (isAP) {
ctx.beginPath();
ctx.arc(x, y, R + 5, 0, 2 * Math.PI);
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 3;
ctx.stroke();
}
ctx.beginPath();
ctx.arc(x, y, R, 0, 2 * Math.PI);
ctx.fillStyle = isAP ? '#fde8ea' : '#1d3557';
ctx.fill();
ctx.strokeStyle = isAP ? '#e63946' : '#1d3557';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = isAP ? '#c92f3c' : '#fff';
ctx.font = 'bold 12px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(i, x, y);
});
}
function setStatus(text, cls) {
const el = document.getElementById('status');
el.textContent = text;
el.className = 'status ' + (cls || '');
}
function analyse() {
tarjan(POS.length, edges);
draw();
const bCount = bridges.size;
const aCount = artPoints.size;
if (bCount === 0 && aCount === 0) {
setStatus('No bridges or articulation points — the graph is 2-connected!', 'ok');
} else {
const bList = bCount
? [...bridges].sort().map(k => '(' + k.replace(',', '–') + ')').join(', ')
: 'none';
const aList = aCount
? [...artPoints].sort((a, b) => a - b).join(', ')
: 'none';
setStatus(fmt('Bridges: %bList%. Articulation points: vertex %aList%.', { bList: bList, aList: aList }), 'bridge');
}
}
function addRandomEdge() {
const n = POS.length;
const existing = new Set(edges.map(([a, b]) => Math.min(a, b) + ',' + Math.max(a, b)));
const candidates = [];
for (let a = 0; a < n; a++)
for (let b = a + 1; b < n; b++)
if (!existing.has(a + ',' + b)) candidates.push([a, b]);
if (!candidates.length) {
setStatus('Graph is already complete — no more edges to add.', 'ok');
return;
}
const pick = candidates[Math.floor(Math.random() * candidates.length)];
edges.push(pick);
bridges = new Set(); artPoints = new Set();
draw();
setStatus(fmt('Added edge (%v%–%v2%). Press Find to re-analyse.', { v: pick[0], v2: pick[1] }));
}
function resetGraph() {
edges = DEFAULT_EDGES.map(e => [...e]);
bridges = new Set(); artPoints = new Set();
draw();
setStatus('Graph reset. Press the button to analyse.');
}
document.getElementById('find').addEventListener('click', analyse);
document.getElementById('addedge').addEventListener('click', addRandomEdge);
document.getElementById('reset').addEventListener('click', resetGraph);
draw(); // initial plain draw
console.log('Bridges & Articulation Points demo ready.');
console.log('Default bridges: 3-4, 3-5, 4-6, 5-7. Articulation points: 3, 4, 5.');
Watch how adding a single edge can eliminate multiple bridges at once — redundancy compounds quickly. Removing bridges one at a time shows how the graph fractures into isolated components.
Comments
Loading comments...