The tree below has seven nodes, each carrying a value. Press Run DFS to watch the depth-first search assign in-times and out-times. Then click any node and press Query subtree sum to see how its subtree becomes a contiguous range in the flat array.
<p class="hint">A rooted tree with 7 nodes (values shown). Press <b>Run DFS</b> to assign in/out times,
then click a node and press <b>Query subtree sum</b> to see the Euler-tour range in action.</p>
<div class="layout">
<svg id="tree-svg" viewBox="0 0 340 220" width="340" height="220"></svg>
<div class="panel">
<div id="tour-display" class="tour-box">
<div class="tour-label">Tour array (in-order DFS):</div>
<div id="tour-cells" class="tour-cells"></div>
</div>
<div class="info-row">
<span class="info-label">Selected node:</span>
<span id="sel-name">โ</span>
</div>
<div class="info-row">
<span class="info-label">in-time / out-time:</span>
<span id="sel-range">โ</span>
</div>
<div class="info-row">
<span class="info-label">Subtree sum:</span>
<span id="sel-sum">โ</span>
</div>
<div class="status" id="status">Press Run DFS to start.</div>
<div class="btns">
<button id="btn-dfs" type="button">Run DFS</button>
<button id="btn-query" type="button" disabled>Query subtree sum</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
</div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 4px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.layout { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; }
svg { flex-shrink: 0; }
.panel { flex: 1; min-width: 180px; }
.tour-box { margin-bottom: .6rem; }
.tour-label { font-size: .78rem; color: #666; margin-bottom: 3px; }
.tour-cells { display: flex; gap: 3px; flex-wrap: wrap; }
.tc { width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;
font: 700 13px ui-monospace, monospace; border-radius: 5px;
background: #e8eef3; border: 1px solid #cdd9e3; color: #1d3557; transition: background .2s; }
.tc.highlight { background: #fde68a; border-color: #f59e0b; }
.tc.active { background: #1d3557; color: #fff; border-color: #1d3557; }
.info-row { display: flex; gap: 6px; font-size: .85rem; margin-bottom: 3px; }
.info-label { color: #666; }
.status { font-size: .92rem; font-weight: 600; margin: .4rem 0; min-height: 1.3em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; margin-top: .4rem; }
button { font: 600 13px system-ui; padding: .38rem .75rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button:disabled { opacity: .45; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
/* SVG node styles injected via JS */
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Euler Tour technique: DFS assigns in-time (entry) and out-time (exit) to each node.
// The subtree of node v is exactly the contiguous range [in(v), out(v)] in the tour array.
// Subtree sum = prefix[out(v)+1] - prefix[in(v)]. O(1) query after O(n) build.
// Tree definition: id, label, value, children
const NODES = {
0: { label: 'A', val: 5, children: [1, 2] },
1: { label: 'B', val: 3, children: [3, 4] },
2: { label: 'C', val: 7, children: [5, 6] },
3: { label: 'D', val: 2, children: [] },
4: { label: 'E', val: 4, children: [] },
5: { label: 'F', val: 1, children: [] },
6: { label: 'G', val: 6, children: [] }
};
const ROOT = 0;
// Node positions in SVG (x, y)
const POS = {
0: [170, 28],
1: [85, 90],
2: [255, 90],
3: [40, 168],
4: [130, 168],
5: [210, 168],
6: [300, 168]
};
// State
let inTime = {}, outTime = {}, tourArr = [], prefix = [];
let selected = null, dfsRan = false;
function dfs(id, timer) {
inTime[id] = timer++;
tourArr.push(id);
for (const child of NODES[id].children) {
timer = dfs(child, timer);
}
outTime[id] = timer - 1;
return timer;
}
function buildTour() {
inTime = {}; outTime = {}; tourArr = []; prefix = [];
dfs(ROOT, 0);
// Build prefix sums over values
prefix[0] = 0;
for (let i = 0; i < tourArr.length; i++) {
prefix[i + 1] = prefix[i] + NODES[tourArr[i]].val;
}
}
function subtreeSum(id) {
// sum of values in subtree of id = prefix[out(id)+1] - prefix[in(id)]
return prefix[outTime[id] + 1] - prefix[inTime[id]];
}
// โโ SVG rendering โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const NS = 'http://www.w3.org/2000/svg';
const svg = document.getElementById('tree-svg');
function makeSVGEl(tag, attrs) {
const el = document.createElementNS(NS, tag);
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
return el;
}
function renderSVG() {
svg.innerHTML = '';
// Draw edges first
for (const [id, node] of Object.entries(NODES)) {
for (const child of node.children) {
const [x1, y1] = POS[id];
const [x2, y2] = POS[child];
svg.appendChild(makeSVGEl('line', { x1, y1, x2, y2, stroke: '#94a3b8', 'stroke-width': 1.8 }));
}
}
// Draw nodes
for (const [id, node] of Object.entries(NODES)) {
const [cx, cy] = POS[id];
const nid = Number(id);
const isSelected = nid === selected;
const hasTime = dfsRan && inTime[nid] !== undefined;
const fill = isSelected ? '#1d3557' : (hasTime ? '#e8eef3' : '#c9ccd1');
const stroke = isSelected ? '#1d3557' : '#94a3b8';
const textFill = isSelected ? '#fff' : '#1d3557';
const g = makeSVGEl('g', { style: 'cursor:pointer' });
g.addEventListener('click', () => selectNode(nid));
g.appendChild(makeSVGEl('circle', { cx, cy, r: 22, fill, stroke, 'stroke-width': 1.8 }));
// Label (A, B, Cโฆ)
const lbl = makeSVGEl('text', { x: cx, y: cy - 4, 'text-anchor': 'middle',
'font-size': 13, 'font-weight': 700, fill: textFill, 'font-family': 'system-ui' });
lbl.textContent = node.label + '=' + node.val;
g.appendChild(lbl);
// In/out times below label
if (hasTime) {
const t = makeSVGEl('text', { x: cx, y: cy + 11, 'text-anchor': 'middle',
'font-size': 9.5, fill: isSelected ? '#cdd9e3' : '#64748b', 'font-family': 'ui-monospace, monospace' });
t.textContent = inTime[nid] + 'โ' + outTime[nid];
g.appendChild(t);
}
svg.appendChild(g);
}
}
// โโ Tour cells โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function renderTour() {
const container = document.getElementById('tour-cells');
container.innerHTML = '';
if (!dfsRan) { container.textContent = 'โ'; return; }
const lo = selected !== null ? inTime[selected] : -1;
const hi = selected !== null ? outTime[selected] : -2;
tourArr.forEach((id, i) => {
const d = document.createElement('div');
d.className = 'tc' + (i >= lo && i <= hi ? ' highlight' : '') + (i === lo ? ' active' : '');
d.title = fmt('pos %i%: %label%(%val%)', { i: i, label: NODES[id].label, val: NODES[id].val });
d.textContent = NODES[id].label;
container.appendChild(d);
});
}
// โโ Interaction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function selectNode(id) {
if (!dfsRan) {
setStatus('Run DFS first to assign times.', 'bad');
return;
}
selected = id;
document.getElementById('sel-name').textContent = fmt('%label% (value=%val%)', { label: NODES[id].label, val: NODES[id].val });
document.getElementById('sel-range').textContent = inTime[id] + ' / ' + outTime[id];
document.getElementById('sel-sum').textContent = 'โ';
document.getElementById('btn-query').disabled = false;
setStatus('Click "Query subtree sum" to compute.', '');
renderSVG();
renderTour();
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status ' + cls;
}
document.getElementById('btn-dfs').addEventListener('click', () => {
buildTour();
dfsRan = true;
selected = null;
document.getElementById('sel-name').textContent = 'โ';
document.getElementById('sel-range').textContent = 'โ';
document.getElementById('sel-sum').textContent = 'โ';
document.getElementById('btn-query').disabled = true;
setStatus('DFS complete! Click a node to select it.', 'ok');
renderSVG();
renderTour();
// Log verification data
console.log('Tour:', tourArr.map(i => NODES[i].label).join(' โ '));
console.log('in-times:', Object.fromEntries(Object.entries(inTime).map(([k,v])=>[NODES[k].label,v])));
console.log('out-times:', Object.fromEntries(Object.entries(outTime).map(([k,v])=>[NODES[k].label,v])));
console.log('prefix sums:', prefix);
});
document.getElementById('btn-query').addEventListener('click', () => {
if (selected === null) return;
const sum = subtreeSum(selected);
document.getElementById('sel-sum').textContent = sum;
setStatus(
fmt('Subtree of %label%: range [%v%, %v2%] โ sum = %sum%', { label: NODES[selected].label, v: inTime[selected], v2: outTime[selected], sum: sum }), 'ok'
);
renderTour();
// Verify: naive sum
function naiveSum(id) {
let s = NODES[id].val;
for (const c of NODES[id].children) s += naiveSum(c);
return s;
}
const naive = naiveSum(selected);
console.assert(sum === naive,
fmt('FAIL: euler sum=%sum% vs naive=%naive% for node %label%', { sum: sum, naive: naive, label: NODES[selected].label }));
console.log(fmt('Euler sum for %label%=%sum% (naive=%naive%) โ', { label: NODES[selected].label, sum: sum, naive: naive }));
});
document.getElementById('btn-reset').addEventListener('click', () => {
dfsRan = false; selected = null;
inTime = {}; outTime = {}; tourArr = []; prefix = [];
document.getElementById('sel-name').textContent = 'โ';
document.getElementById('sel-range').textContent = 'โ';
document.getElementById('sel-sum').textContent = 'โ';
document.getElementById('btn-query').disabled = true;
setStatus('Press Run DFS to start.', '');
renderSVG();
renderTour();
});
// Initial render
renderSVG();
renderTour();
<p class="hint">A rooted tree with 7 nodes (values shown). Press <b>Run DFS</b> to assign in/out times,
then click a node and press <b>Query subtree sum</b> to see the Euler-tour range in action.</p>
<div class="layout">
<svg id="tree-svg" viewBox="0 0 340 220" width="340" height="220"></svg>
<div class="panel">
<div id="tour-display" class="tour-box">
<div class="tour-label">Tour array (in-order DFS):</div>
<div id="tour-cells" class="tour-cells"></div>
</div>
<div class="info-row">
<span class="info-label">Selected node:</span>
<span id="sel-name">โ</span>
</div>
<div class="info-row">
<span class="info-label">in-time / out-time:</span>
<span id="sel-range">โ</span>
</div>
<div class="info-row">
<span class="info-label">Subtree sum:</span>
<span id="sel-sum">โ</span>
</div>
<div class="status" id="status">Press Run DFS to start.</div>
<div class="btns">
<button id="btn-dfs" type="button">Run DFS</button>
<button id="btn-query" type="button" disabled>Query subtree sum</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
</div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 4px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.layout { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; }
svg { flex-shrink: 0; }
.panel { flex: 1; min-width: 180px; }
.tour-box { margin-bottom: .6rem; }
.tour-label { font-size: .78rem; color: #666; margin-bottom: 3px; }
.tour-cells { display: flex; gap: 3px; flex-wrap: wrap; }
.tc { width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;
font: 700 13px ui-monospace, monospace; border-radius: 5px;
background: #e8eef3; border: 1px solid #cdd9e3; color: #1d3557; transition: background .2s; }
.tc.highlight { background: #fde68a; border-color: #f59e0b; }
.tc.active { background: #1d3557; color: #fff; border-color: #1d3557; }
.info-row { display: flex; gap: 6px; font-size: .85rem; margin-bottom: 3px; }
.info-label { color: #666; }
.status { font-size: .92rem; font-weight: 600; margin: .4rem 0; min-height: 1.3em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; margin-top: .4rem; }
button { font: 600 13px system-ui; padding: .38rem .75rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button:disabled { opacity: .45; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
/* SVG node styles injected via JS */
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Euler Tour technique: DFS assigns in-time (entry) and out-time (exit) to each node.
// The subtree of node v is exactly the contiguous range [in(v), out(v)] in the tour array.
// Subtree sum = prefix[out(v)+1] - prefix[in(v)]. O(1) query after O(n) build.
// Tree definition: id, label, value, children
const NODES = {
0: { label: 'A', val: 5, children: [1, 2] },
1: { label: 'B', val: 3, children: [3, 4] },
2: { label: 'C', val: 7, children: [5, 6] },
3: { label: 'D', val: 2, children: [] },
4: { label: 'E', val: 4, children: [] },
5: { label: 'F', val: 1, children: [] },
6: { label: 'G', val: 6, children: [] }
};
const ROOT = 0;
// Node positions in SVG (x, y)
const POS = {
0: [170, 28],
1: [85, 90],
2: [255, 90],
3: [40, 168],
4: [130, 168],
5: [210, 168],
6: [300, 168]
};
// State
let inTime = {}, outTime = {}, tourArr = [], prefix = [];
let selected = null, dfsRan = false;
function dfs(id, timer) {
inTime[id] = timer++;
tourArr.push(id);
for (const child of NODES[id].children) {
timer = dfs(child, timer);
}
outTime[id] = timer - 1;
return timer;
}
function buildTour() {
inTime = {}; outTime = {}; tourArr = []; prefix = [];
dfs(ROOT, 0);
// Build prefix sums over values
prefix[0] = 0;
for (let i = 0; i < tourArr.length; i++) {
prefix[i + 1] = prefix[i] + NODES[tourArr[i]].val;
}
}
function subtreeSum(id) {
// sum of values in subtree of id = prefix[out(id)+1] - prefix[in(id)]
return prefix[outTime[id] + 1] - prefix[inTime[id]];
}
// โโ SVG rendering โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const NS = 'http://www.w3.org/2000/svg';
const svg = document.getElementById('tree-svg');
function makeSVGEl(tag, attrs) {
const el = document.createElementNS(NS, tag);
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
return el;
}
function renderSVG() {
svg.innerHTML = '';
// Draw edges first
for (const [id, node] of Object.entries(NODES)) {
for (const child of node.children) {
const [x1, y1] = POS[id];
const [x2, y2] = POS[child];
svg.appendChild(makeSVGEl('line', { x1, y1, x2, y2, stroke: '#94a3b8', 'stroke-width': 1.8 }));
}
}
// Draw nodes
for (const [id, node] of Object.entries(NODES)) {
const [cx, cy] = POS[id];
const nid = Number(id);
const isSelected = nid === selected;
const hasTime = dfsRan && inTime[nid] !== undefined;
const fill = isSelected ? '#1d3557' : (hasTime ? '#e8eef3' : '#c9ccd1');
const stroke = isSelected ? '#1d3557' : '#94a3b8';
const textFill = isSelected ? '#fff' : '#1d3557';
const g = makeSVGEl('g', { style: 'cursor:pointer' });
g.addEventListener('click', () => selectNode(nid));
g.appendChild(makeSVGEl('circle', { cx, cy, r: 22, fill, stroke, 'stroke-width': 1.8 }));
// Label (A, B, Cโฆ)
const lbl = makeSVGEl('text', { x: cx, y: cy - 4, 'text-anchor': 'middle',
'font-size': 13, 'font-weight': 700, fill: textFill, 'font-family': 'system-ui' });
lbl.textContent = node.label + '=' + node.val;
g.appendChild(lbl);
// In/out times below label
if (hasTime) {
const t = makeSVGEl('text', { x: cx, y: cy + 11, 'text-anchor': 'middle',
'font-size': 9.5, fill: isSelected ? '#cdd9e3' : '#64748b', 'font-family': 'ui-monospace, monospace' });
t.textContent = inTime[nid] + 'โ' + outTime[nid];
g.appendChild(t);
}
svg.appendChild(g);
}
}
// โโ Tour cells โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function renderTour() {
const container = document.getElementById('tour-cells');
container.innerHTML = '';
if (!dfsRan) { container.textContent = 'โ'; return; }
const lo = selected !== null ? inTime[selected] : -1;
const hi = selected !== null ? outTime[selected] : -2;
tourArr.forEach((id, i) => {
const d = document.createElement('div');
d.className = 'tc' + (i >= lo && i <= hi ? ' highlight' : '') + (i === lo ? ' active' : '');
d.title = fmt('pos %i%: %label%(%val%)', { i: i, label: NODES[id].label, val: NODES[id].val });
d.textContent = NODES[id].label;
container.appendChild(d);
});
}
// โโ Interaction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function selectNode(id) {
if (!dfsRan) {
setStatus('Run DFS first to assign times.', 'bad');
return;
}
selected = id;
document.getElementById('sel-name').textContent = fmt('%label% (value=%val%)', { label: NODES[id].label, val: NODES[id].val });
document.getElementById('sel-range').textContent = inTime[id] + ' / ' + outTime[id];
document.getElementById('sel-sum').textContent = 'โ';
document.getElementById('btn-query').disabled = false;
setStatus('Click "Query subtree sum" to compute.', '');
renderSVG();
renderTour();
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status ' + cls;
}
document.getElementById('btn-dfs').addEventListener('click', () => {
buildTour();
dfsRan = true;
selected = null;
document.getElementById('sel-name').textContent = 'โ';
document.getElementById('sel-range').textContent = 'โ';
document.getElementById('sel-sum').textContent = 'โ';
document.getElementById('btn-query').disabled = true;
setStatus('DFS complete! Click a node to select it.', 'ok');
renderSVG();
renderTour();
// Log verification data
console.log('Tour:', tourArr.map(i => NODES[i].label).join(' โ '));
console.log('in-times:', Object.fromEntries(Object.entries(inTime).map(([k,v])=>[NODES[k].label,v])));
console.log('out-times:', Object.fromEntries(Object.entries(outTime).map(([k,v])=>[NODES[k].label,v])));
console.log('prefix sums:', prefix);
});
document.getElementById('btn-query').addEventListener('click', () => {
if (selected === null) return;
const sum = subtreeSum(selected);
document.getElementById('sel-sum').textContent = sum;
setStatus(
fmt('Subtree of %label%: range [%v%, %v2%] โ sum = %sum%', { label: NODES[selected].label, v: inTime[selected], v2: outTime[selected], sum: sum }), 'ok'
);
renderTour();
// Verify: naive sum
function naiveSum(id) {
let s = NODES[id].val;
for (const c of NODES[id].children) s += naiveSum(c);
return s;
}
const naive = naiveSum(selected);
console.assert(sum === naive,
fmt('FAIL: euler sum=%sum% vs naive=%naive% for node %label%', { sum: sum, naive: naive, label: NODES[selected].label }));
console.log(fmt('Euler sum for %label%=%sum% (naive=%naive%) โ', { label: NODES[selected].label, sum: sum, naive: naive }));
});
document.getElementById('btn-reset').addEventListener('click', () => {
dfsRan = false; selected = null;
inTime = {}; outTime = {}; tourArr = []; prefix = [];
document.getElementById('sel-name').textContent = 'โ';
document.getElementById('sel-range').textContent = 'โ';
document.getElementById('sel-sum').textContent = 'โ';
document.getElementById('btn-query').disabled = true;
setStatus('Press Run DFS to start.', '');
renderSVG();
renderTour();
});
// Initial render
renderSVG();
renderTour();
Comments
Loading comments...