The tree below has pre-assigned edge weights. Click any two nodes to query the path sum between them. The demo runs HLD, highlights each heavy chain segment it touches (in orange), and counts how many separate chain intervals the path decomposes into.
<p class="hint">Click two nodes to query the path sum. Orange edges and nodes belong to heavy chains touched by the query. Watch how few chain segments are needed.</p>
<div class="canvas-wrap">
<canvas id="c" width="480" height="300"></canvas>
</div>
<div class="info" id="info">Select a start node.</div>
<div class="btns">
<button id="resetBtn" type="button" class="ghost">Reset selection</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-wrap { width: 100%; overflow-x: auto; }
canvas { display: block; background: #f4f7fa; border-radius: 10px; border: 1px solid #d0dae3; cursor: pointer; max-width: 100%; }
.info { font-size: .95rem; font-weight: 600; margin: .55rem 0; min-height: 1.5em; color: #1d3557; white-space: pre-wrap; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui; padding: .42rem .85rem; 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] ?? ''));
// Heavy-Light Decomposition demo.
// Tree is fixed: 9 nodes (0-8), rooted at 0.
// Edges: 0-1(w4), 0-2(w3), 1-3(w2), 1-4(w7), 2-5(w1), 2-6(w5), 3-7(w6), 3-8(w9)
// Heavy child of each node = child with largest subtree size.
const N = 9;
const ROOT = 0;
// Adjacency list: [neighbor, edgeWeight]
const adj = [
[[1,4],[2,3]], // 0
[[0,4],[3,2],[4,7]], // 1
[[0,3],[5,1],[6,5]], // 2
[[1,2],[7,6],[8,9]], // 3
[[1,7]], // 4
[[2,1]], // 5
[[2,5]], // 6
[[3,6]], // 7
[[3,9]] // 8
];
// ââ HLD preprocessing ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
const parent = new Array(N).fill(-1);
const depth = new Array(N).fill(0);
const sz = new Array(N).fill(1);
const heavy = new Array(N).fill(-1); // heavy child
const head = new Array(N).fill(0); // chain head
const pos = new Array(N).fill(0); // position in flat array
const edgeW = {}; // edgeKey -> weight (u<v)
// Build edge-weight map
for (let u = 0; u < N; u++) {
for (const [v, w] of adj[u]) {
const k = Math.min(u,v) + ',' + Math.max(u,v);
edgeW[k] = w;
}
}
function ew(u, v) { return edgeW[Math.min(u,v)+','+Math.max(u,v)] || 0; }
// DFS1: compute parent, depth, subtree size, heavy child
function dfs1(u, p, d) {
parent[u] = p; depth[u] = d; sz[u] = 1;
let maxSz = 0;
for (const [v] of adj[u]) {
if (v === p) continue;
dfs1(v, u, d + 1);
sz[u] += sz[v];
if (sz[v] > maxSz) { maxSz = sz[v]; heavy[u] = v; }
}
}
dfs1(ROOT, -1, 0);
// DFS2: assign chain heads and flat positions
let curPos = 0;
function dfs2(u, h) {
head[u] = h; pos[u] = curPos++;
if (heavy[u] !== -1) dfs2(heavy[u], h);
for (const [v] of adj[u]) {
if (v === parent[u] || v === heavy[u]) continue;
dfs2(v, v); // start a new chain
}
}
dfs2(ROOT, ROOT);
// Segment tree on node weights (we store edge weight on the deeper node)
// nodeVal[u] = weight of edge from parent[u] to u (0 for root)
const nodeVal = new Array(N).fill(0);
for (let u = 1; u < N; u++) nodeVal[u] = ew(u, parent[u]);
// Simple O(n) range sum for demo clarity (HLD guarantees O(log n) intervals)
function rangeSum(l, r, posToNode) {
let s = 0;
for (let i = l; i <= r; i++) s += nodeVal[posToNode[i]];
return s;
}
// Build inverse pos map
const posToNode = new Array(N);
for (let u = 0; u < N; u++) posToNode[pos[u]] = u;
// ââ Path query: returns {sum, segments: [[u,v], ...]} ââââââââââââââââââââ
function pathQuery(u, v) {
let sum = 0;
const segments = []; // each: {nodes: Set of nodes in this chain interval}
while (head[u] !== head[v]) {
if (depth[head[u]] < depth[head[v]]) { let t=u; u=v; v=t; }
// u's chain head is deeper: process from head[u] to u
const chainNodes = new Set();
for (let x = head[u]; ; x = parent[x] === -1 ? -1 : (pos[x] <= pos[u] ? x : -1)) {
// collect nodes in pos range [pos[head[u]], pos[u]]
break;
}
for (let p = pos[head[u]]; p <= pos[u]; p++) chainNodes.add(posToNode[p]);
sum += rangeSum(pos[head[u]], pos[u], posToNode);
segments.push(chainNodes);
u = parent[head[u]];
}
// same chain now
if (depth[u] > depth[v]) { let t=u; u=v; v=t; }
// range [pos[u]+1, pos[v]] (exclude LCA for edge-weight semantics, include if u===v)
const lo = pos[u] + (u === v ? 0 : 1);
const hi = pos[v];
if (lo <= hi) {
const chainNodes = new Set();
for (let p = lo; p <= hi; p++) chainNodes.add(posToNode[p]);
sum += rangeSum(lo, hi, posToNode);
if (chainNodes.size > 0) segments.push(chainNodes);
}
return { sum, segments };
}
// ââ Layout (fixed node positions for clarity) ââââââââââââââââââââ
const POS = [
[240, 30], // 0
[140, 100], // 1
[340, 100], // 2
[80, 180], // 3
[200, 180], // 4
[290, 180], // 5
[400, 180], // 6
[40, 260], // 7
[130, 260], // 8
];
const R = 18;
// ââ Drawing ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let sel1 = -1, sel2 = -1;
let highlightNodes = new Set();
let highlightEdges = new Set(); // "u,v" with u<v
let queryResult = null;
function edgeHighlighted(u, v) {
return highlightEdges.has(Math.min(u,v)+','+Math.max(u,v));
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// edges
for (let u = 0; u < N; u++) {
for (const [v] of adj[u]) {
if (v <= u) continue;
const isHeavy = heavy[u] === v || heavy[v] === u;
const isHigh = edgeHighlighted(u, v);
ctx.beginPath();
ctx.moveTo(POS[u][0], POS[u][1]);
ctx.lineTo(POS[v][0], POS[v][1]);
ctx.lineWidth = isHigh ? 4 : (isHeavy ? 3 : 1.5);
ctx.strokeStyle = isHigh ? '#e07800' : (isHeavy ? '#457b9d' : '#b0bec8');
ctx.setLineDash(isHeavy ? [] : [5,4]);
ctx.stroke();
ctx.setLineDash([]);
// edge weight label
const mx = (POS[u][0]+POS[v][0])/2, my = (POS[u][1]+POS[v][1])/2;
ctx.fillStyle = isHigh ? '#e07800' : '#555';
ctx.font = isHigh ? 'bold 11px system-ui' : '10px system-ui';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(ew(u,v), mx + 7, my - 7);
}
}
// nodes
for (let u = 0; u < N; u++) {
const [x,y] = POS[u];
const isSel = u === sel1 || u === sel2;
const isHigh = highlightNodes.has(u);
ctx.beginPath(); ctx.arc(x, y, R, 0, 2*Math.PI);
ctx.fillStyle = isSel ? '#1d3557' : (isHigh ? '#e07800' : '#e8eef3');
ctx.strokeStyle = isSel ? '#1d3557' : (isHigh ? '#c05e00' : '#457b9d');
ctx.lineWidth = isSel || isHigh ? 2.5 : 1.5;
ctx.fill(); ctx.stroke();
ctx.fillStyle = isSel || isHigh ? '#fff' : '#1d3557';
ctx.font = 'bold 13px system-ui';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(u, x, y);
}
// legend
ctx.font = '10px system-ui'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
ctx.strokeStyle = '#457b9d'; ctx.lineWidth = 2.5; ctx.setLineDash([]);
ctx.beginPath(); ctx.moveTo(8,288); ctx.lineTo(28,288); ctx.stroke();
ctx.fillStyle = '#457b9d'; ctx.fillText('heavy edge', 32, 288);
ctx.strokeStyle = '#b0bec8'; ctx.lineWidth = 1.5; ctx.setLineDash([5,4]);
ctx.beginPath(); ctx.moveTo(110,288); ctx.lineTo(130,288); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#666'; ctx.fillText('light edge', 134, 288);
}
draw();
// ââ Interaction ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
canvas.addEventListener('click', e => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const mx = (e.clientX - rect.left) * scaleX;
const my = (e.clientY - rect.top) * scaleY;
let hit = -1;
for (let u = 0; u < N; u++) {
const dx = POS[u][0]-mx, dy = POS[u][1]-my;
if (dx*dx+dy*dy <= (R+4)*(R+4)) { hit = u; break; }
}
if (hit === -1) return;
if (sel1 === -1) {
sel1 = hit; sel2 = -1;
highlightNodes.clear(); highlightEdges.clear();
document.getElementById('info').textContent = fmt('Node %sel1% selected. Now pick the destination node.', { sel1: sel1 });
} else if (sel2 === -1 && hit !== sel1) {
sel2 = hit;
runQuery();
}
draw();
});
function runQuery() {
const { sum, segments } = pathQuery(sel1, sel2);
highlightNodes.clear(); highlightEdges.clear();
// collect all highlighted nodes
for (const seg of segments) for (const nd of seg) highlightNodes.add(nd);
// highlighted edges: any edge (u,v) where both u,v in highlightNodes AND edge is on the path
// We mark edges that connect consecutive nodes on the path (parent-child within highlighted set)
for (const nd of highlightNodes) {
if (parent[nd] !== -1 && highlightNodes.has(parent[nd])) {
highlightEdges.add(Math.min(nd,parent[nd])+','+Math.max(nd,parent[nd]));
}
}
const chainCount = segments.length;
document.getElementById('info').textContent =
fmt('Path %sel1% â %sel2% | Sum = %sum% | Chain segments touched: %chainCount% (†O(log n))', { sel1: sel1, sel2: sel2, sum: sum, chainCount: chainCount });
console.log('HLD path', sel1, '->', sel2, '| sum:', sum, '| segments:', chainCount,
'| nodes:', [...highlightNodes].join(','));
draw();
}
document.getElementById('resetBtn').addEventListener('click', () => {
sel1 = -1; sel2 = -1;
highlightNodes.clear(); highlightEdges.clear();
document.getElementById('info').textContent = 'Select a start node.';
draw();
});
<p class="hint">Click two nodes to query the path sum. Orange edges and nodes belong to heavy chains touched by the query. Watch how few chain segments are needed.</p>
<div class="canvas-wrap">
<canvas id="c" width="480" height="300"></canvas>
</div>
<div class="info" id="info">Select a start node.</div>
<div class="btns">
<button id="resetBtn" type="button" class="ghost">Reset selection</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-wrap { width: 100%; overflow-x: auto; }
canvas { display: block; background: #f4f7fa; border-radius: 10px; border: 1px solid #d0dae3; cursor: pointer; max-width: 100%; }
.info { font-size: .95rem; font-weight: 600; margin: .55rem 0; min-height: 1.5em; color: #1d3557; white-space: pre-wrap; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui; padding: .42rem .85rem; 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] ?? ''));
// Heavy-Light Decomposition demo.
// Tree is fixed: 9 nodes (0-8), rooted at 0.
// Edges: 0-1(w4), 0-2(w3), 1-3(w2), 1-4(w7), 2-5(w1), 2-6(w5), 3-7(w6), 3-8(w9)
// Heavy child of each node = child with largest subtree size.
const N = 9;
const ROOT = 0;
// Adjacency list: [neighbor, edgeWeight]
const adj = [
[[1,4],[2,3]], // 0
[[0,4],[3,2],[4,7]], // 1
[[0,3],[5,1],[6,5]], // 2
[[1,2],[7,6],[8,9]], // 3
[[1,7]], // 4
[[2,1]], // 5
[[2,5]], // 6
[[3,6]], // 7
[[3,9]] // 8
];
// ââ HLD preprocessing ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
const parent = new Array(N).fill(-1);
const depth = new Array(N).fill(0);
const sz = new Array(N).fill(1);
const heavy = new Array(N).fill(-1); // heavy child
const head = new Array(N).fill(0); // chain head
const pos = new Array(N).fill(0); // position in flat array
const edgeW = {}; // edgeKey -> weight (u<v)
// Build edge-weight map
for (let u = 0; u < N; u++) {
for (const [v, w] of adj[u]) {
const k = Math.min(u,v) + ',' + Math.max(u,v);
edgeW[k] = w;
}
}
function ew(u, v) { return edgeW[Math.min(u,v)+','+Math.max(u,v)] || 0; }
// DFS1: compute parent, depth, subtree size, heavy child
function dfs1(u, p, d) {
parent[u] = p; depth[u] = d; sz[u] = 1;
let maxSz = 0;
for (const [v] of adj[u]) {
if (v === p) continue;
dfs1(v, u, d + 1);
sz[u] += sz[v];
if (sz[v] > maxSz) { maxSz = sz[v]; heavy[u] = v; }
}
}
dfs1(ROOT, -1, 0);
// DFS2: assign chain heads and flat positions
let curPos = 0;
function dfs2(u, h) {
head[u] = h; pos[u] = curPos++;
if (heavy[u] !== -1) dfs2(heavy[u], h);
for (const [v] of adj[u]) {
if (v === parent[u] || v === heavy[u]) continue;
dfs2(v, v); // start a new chain
}
}
dfs2(ROOT, ROOT);
// Segment tree on node weights (we store edge weight on the deeper node)
// nodeVal[u] = weight of edge from parent[u] to u (0 for root)
const nodeVal = new Array(N).fill(0);
for (let u = 1; u < N; u++) nodeVal[u] = ew(u, parent[u]);
// Simple O(n) range sum for demo clarity (HLD guarantees O(log n) intervals)
function rangeSum(l, r, posToNode) {
let s = 0;
for (let i = l; i <= r; i++) s += nodeVal[posToNode[i]];
return s;
}
// Build inverse pos map
const posToNode = new Array(N);
for (let u = 0; u < N; u++) posToNode[pos[u]] = u;
// ââ Path query: returns {sum, segments: [[u,v], ...]} ââââââââââââââââââââ
function pathQuery(u, v) {
let sum = 0;
const segments = []; // each: {nodes: Set of nodes in this chain interval}
while (head[u] !== head[v]) {
if (depth[head[u]] < depth[head[v]]) { let t=u; u=v; v=t; }
// u's chain head is deeper: process from head[u] to u
const chainNodes = new Set();
for (let x = head[u]; ; x = parent[x] === -1 ? -1 : (pos[x] <= pos[u] ? x : -1)) {
// collect nodes in pos range [pos[head[u]], pos[u]]
break;
}
for (let p = pos[head[u]]; p <= pos[u]; p++) chainNodes.add(posToNode[p]);
sum += rangeSum(pos[head[u]], pos[u], posToNode);
segments.push(chainNodes);
u = parent[head[u]];
}
// same chain now
if (depth[u] > depth[v]) { let t=u; u=v; v=t; }
// range [pos[u]+1, pos[v]] (exclude LCA for edge-weight semantics, include if u===v)
const lo = pos[u] + (u === v ? 0 : 1);
const hi = pos[v];
if (lo <= hi) {
const chainNodes = new Set();
for (let p = lo; p <= hi; p++) chainNodes.add(posToNode[p]);
sum += rangeSum(lo, hi, posToNode);
if (chainNodes.size > 0) segments.push(chainNodes);
}
return { sum, segments };
}
// ââ Layout (fixed node positions for clarity) ââââââââââââââââââââ
const POS = [
[240, 30], // 0
[140, 100], // 1
[340, 100], // 2
[80, 180], // 3
[200, 180], // 4
[290, 180], // 5
[400, 180], // 6
[40, 260], // 7
[130, 260], // 8
];
const R = 18;
// ââ Drawing ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let sel1 = -1, sel2 = -1;
let highlightNodes = new Set();
let highlightEdges = new Set(); // "u,v" with u<v
let queryResult = null;
function edgeHighlighted(u, v) {
return highlightEdges.has(Math.min(u,v)+','+Math.max(u,v));
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// edges
for (let u = 0; u < N; u++) {
for (const [v] of adj[u]) {
if (v <= u) continue;
const isHeavy = heavy[u] === v || heavy[v] === u;
const isHigh = edgeHighlighted(u, v);
ctx.beginPath();
ctx.moveTo(POS[u][0], POS[u][1]);
ctx.lineTo(POS[v][0], POS[v][1]);
ctx.lineWidth = isHigh ? 4 : (isHeavy ? 3 : 1.5);
ctx.strokeStyle = isHigh ? '#e07800' : (isHeavy ? '#457b9d' : '#b0bec8');
ctx.setLineDash(isHeavy ? [] : [5,4]);
ctx.stroke();
ctx.setLineDash([]);
// edge weight label
const mx = (POS[u][0]+POS[v][0])/2, my = (POS[u][1]+POS[v][1])/2;
ctx.fillStyle = isHigh ? '#e07800' : '#555';
ctx.font = isHigh ? 'bold 11px system-ui' : '10px system-ui';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(ew(u,v), mx + 7, my - 7);
}
}
// nodes
for (let u = 0; u < N; u++) {
const [x,y] = POS[u];
const isSel = u === sel1 || u === sel2;
const isHigh = highlightNodes.has(u);
ctx.beginPath(); ctx.arc(x, y, R, 0, 2*Math.PI);
ctx.fillStyle = isSel ? '#1d3557' : (isHigh ? '#e07800' : '#e8eef3');
ctx.strokeStyle = isSel ? '#1d3557' : (isHigh ? '#c05e00' : '#457b9d');
ctx.lineWidth = isSel || isHigh ? 2.5 : 1.5;
ctx.fill(); ctx.stroke();
ctx.fillStyle = isSel || isHigh ? '#fff' : '#1d3557';
ctx.font = 'bold 13px system-ui';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(u, x, y);
}
// legend
ctx.font = '10px system-ui'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
ctx.strokeStyle = '#457b9d'; ctx.lineWidth = 2.5; ctx.setLineDash([]);
ctx.beginPath(); ctx.moveTo(8,288); ctx.lineTo(28,288); ctx.stroke();
ctx.fillStyle = '#457b9d'; ctx.fillText('heavy edge', 32, 288);
ctx.strokeStyle = '#b0bec8'; ctx.lineWidth = 1.5; ctx.setLineDash([5,4]);
ctx.beginPath(); ctx.moveTo(110,288); ctx.lineTo(130,288); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#666'; ctx.fillText('light edge', 134, 288);
}
draw();
// ââ Interaction ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
canvas.addEventListener('click', e => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const mx = (e.clientX - rect.left) * scaleX;
const my = (e.clientY - rect.top) * scaleY;
let hit = -1;
for (let u = 0; u < N; u++) {
const dx = POS[u][0]-mx, dy = POS[u][1]-my;
if (dx*dx+dy*dy <= (R+4)*(R+4)) { hit = u; break; }
}
if (hit === -1) return;
if (sel1 === -1) {
sel1 = hit; sel2 = -1;
highlightNodes.clear(); highlightEdges.clear();
document.getElementById('info').textContent = fmt('Node %sel1% selected. Now pick the destination node.', { sel1: sel1 });
} else if (sel2 === -1 && hit !== sel1) {
sel2 = hit;
runQuery();
}
draw();
});
function runQuery() {
const { sum, segments } = pathQuery(sel1, sel2);
highlightNodes.clear(); highlightEdges.clear();
// collect all highlighted nodes
for (const seg of segments) for (const nd of seg) highlightNodes.add(nd);
// highlighted edges: any edge (u,v) where both u,v in highlightNodes AND edge is on the path
// We mark edges that connect consecutive nodes on the path (parent-child within highlighted set)
for (const nd of highlightNodes) {
if (parent[nd] !== -1 && highlightNodes.has(parent[nd])) {
highlightEdges.add(Math.min(nd,parent[nd])+','+Math.max(nd,parent[nd]));
}
}
const chainCount = segments.length;
document.getElementById('info').textContent =
fmt('Path %sel1% â %sel2% | Sum = %sum% | Chain segments touched: %chainCount% (†O(log n))', { sel1: sel1, sel2: sel2, sum: sum, chainCount: chainCount });
console.log('HLD path', sel1, '->', sel2, '| sum:', sum, '| segments:', chainCount,
'| nodes:', [...highlightNodes].join(','));
draw();
}
document.getElementById('resetBtn').addEventListener('click', () => {
sel1 = -1; sel2 = -1;
highlightNodes.clear(); highlightEdges.clear();
document.getElementById('info').textContent = 'Select a start node.';
draw();
});
Notice that even on a long path the number of highlighted chain segments is small â at most O(logn). That is the guarantee that makes HLD fast regardless of the tree's shape.
Comments
Loading comments...