Flatten the tree in DFS preorder: the subtree of becomes the interval , so every array structure (Fenwick, lazy segment tree, persistent tree, Mo) works on subtrees. Paths are not intervals, but root paths are recovered with entry/exit tricks and LCA (LCA); general path updates and queries go to HLD.
struct EulerTour { // subtree(u) = [tin[u], tout[u]) in preorder
int n; vector<int> tin, tout, order, par, dep; vector<vector<int>> ch; // ch[u] in tin order
EulerTour(const vector<vector<int>>& adj, int root) : n(sz(adj)), tin(n), tout(n), par(n, -1), dep(n), ch(n) {
vector<int> it(n), st{root}; order = {root};
while (sz(st)) {
int u = st.back();
if (it[u] < sz(adj[u])) {
int v = adj[u][it[u]++]; if (v == par[u]) continue;
par[v] = u, dep[v] = dep[u] + 1, tin[v] = sz(order), order.push_back(v), ch[u].push_back(v), st.push_back(v);
} else tout[u] = sz(order), st.pop_back();
}
}
bool isAnc(int u, int v) const { return tin[u] <= tin[v] && tout[v] <= tout[u]; } // u ancestor of v (or equal)
int childToward(int u, int v) const { // child of u whose subtree contains v (u a proper ancestor)
return *prev(upper_bound(all(ch[u]), v, [&](int x, int c) { return tin[x] < tin[c]; }));
}
};Proof: subtrees are intervals
The DFS enters , then visits exactly the vertices of ‘s subtree before leaving (it only moves to children and returns to parents), and every vertex gets the next preorder index when entered. So the subtree occupies the consecutive indices . Ancestry is interval containment. The children of get increasing
tinin the order visited, and their intervals partition , so the child containing is the last one with .
Reductions
| Operation | Array form |
|---|---|
| point update, subtree sum | Fenwick at tin, range sum (CSES “Subtree Queries”) |
| subtree add, point value | range add / point query (difference Fenwick) |
| subtree add, subtree sum / max | lazy segment tree |
| root-path sum with point updates | subtree add of at ‘s interval; = point query at |
| path sum – | with (any group: xor, sums mod ) |
| path XOR – | — the "" term is XOR-ing twice, which is already free (any value XORed with itself cancels), so only the survives |
| path add, point query | difference on the tree: at ; at and ; value = subtree sum (edge-weight variant with the weight pushed to the child endpoint: see tree difference array) |
| path add, subtree sum | two Fenwicks (below) |
| entry , exit tour (size ) | prefix up to = root path; Mo’s on trees (Mo’s Algorithm): map each query to the array range (or plus the LCA, when is not an ancestor of ), then run Mo’s with a toggle instead of add/remove — the first time the pointer crosses a vertex, activate it (e.g. add its colour to a frequency map); the second time (its exit), deactivate it; every vertex off the path is toggled on then off and vanishes, exactly simulating the path |
| vertices at depth in subtree() | per-depth sorted tin lists, two binary searches |
| -th smallest in a subtree | persistent segment tree over the preorder array |
| subtree of after rerooting at | whole tree, or the complement of the child-toward- interval |
| distinct colours in a subtree | offline over intervals (Offline 2D Queries) or small-to-large |
struct PathAddSubtreeSum { // add x on path u - v; sum of a subtree
const EulerTour& T; Fenwick<ll> A, B; // A: c(z), B: c(z) * (dep z + 1)
PathAddSubtreeSum(const EulerTour& T) : T(T), A(T.n), B(T.n) {}
void point(int z, ll c) { if (z < 0) return; A.add(T.tin[z], c); B.add(T.tin[z], c * (T.dep[z] + 1)); }
void pathAdd(int u, int v, int l, ll x) { point(u, x); point(v, x); point(l, -x); point(T.par[l], -x); } // l = lca(u, v)
ll subtreeSum(int w) const { return B.sum(T.tin[w], T.tout[w]) - (ll)T.dep[w] * A.sum(T.tin[w], T.tout[w]); }
};
vector<pair<int, int>> rerootedSubtree(const EulerTour& T, int u, int r) { // subtree of u when rooted at r, as intervals
if (u == r) return {{0, T.n}};
if (!T.isAnc(u, r)) return {{T.tin[u], T.tout[u]}};
int c = T.childToward(u, r);
return {{0, T.tin[c]}, {T.tout[c], T.n}};
}
int countAtDepth(const EulerTour& T, const vector<vector<int>>& byDep, int u, int d) { // byDep[d]: sorted tins
if (d >= sz(byDep)) return 0;
auto& v = byDep[d];
return int(lower_bound(all(v), T.tout[u]) - lower_bound(all(v), T.tin[u]));
}Proof: the path and reroot formulas
Root paths: adding to the interval of ‘s subtree adds to exactly for in that subtree, i.e. those whose root path contains . Path sum: counts the common part above twice and itself twice; subtracting removes the common part and , and puts back. Difference trick: the value at is ; on the path gets (‘s subtree holds one or two of and, when it holds both, also , plus never), and off the path gets 0. Path add, subtree sum: , since is counted once for each between and ; split into the two Fenwicks. Reroot: with root , the subtree of is everything hanging off away from . If is outside ‘s original subtree, that is the original subtree; if is inside, below child , it is everything except ‘s original subtree; if , everything. Depth count: vertices at depth in ‘s subtree are those at depth with
tinin .
The three tours
| Tour | Length | Gives |
|---|---|---|
preorder tin/tout | subtree intervals, ancestry test | |
| entry and exit events | root-path sums by (exit cancels entry), Mo on trees, bracket encoding of the tree shape | |
| Euler walk (vertex after each child) | LCA by depth RMQ, dynamic diameter (Tree Basics), Euler tour trees |
Dynamic versions: keep the walk in a balanced BST to support link/cut with subtree aggregates (Euler Tour Tree).
Entry/exit cancellation needs an invertible operation
The tour’s “side branches cancel” trick relies on being able to undo a value (sum, XOR) when exiting a subtree. It does not work for max, min, matrix products, or string concatenation — there is no way to “un-max” a value once a branch has been explored and left. For a path query on a non-invertible operation, abandon this tour and build HLD instead.