Recursively split a tree at its centroid, building a centroid tree of depth . It is the standard tool for counting or optimising over all paths in a tree.
The centroid
A vertex whose removal leaves every component of size . Every tree has one or two, found in one pass over the subtree sizes:
int findCentroid(int v, int p, int total) {
for (int c : adj[v])
if (c != p && !removed[c] && sz[c] > total / 2)
return findCentroid(c, v, total);
return v;
}The decomposition
void decompose(int v, int parentCentroid) {
computeSizes(v, -1);
int c = findCentroid(v, -1, sz[v]);
removed[c] = true;
cpar[c] = parentCentroid;
solve(c); // handle all paths THROUGH c
for (int u : adj[c])
if (!removed[u]) decompose(u, c);
}Each level halves the component sizes, so the centroid tree has depth and the total work across all levels is (each vertex appears in components).
Why it works for path problems
Every path in the tree passes through exactly one highest centroid in the decomposition — the first centroid removed that lies on it. So if solve(c) correctly counts all paths through within the current component, summing over all centroids counts every path exactly once, with no double counting.
The template for solve
void solve(int c) {
// 1. collect (depth, other data) for every vertex in the component, per branch
// 2. count valid pairs across ALL vertices (this over-counts pairs in the same branch)
// 3. for each branch, count valid pairs WITHIN it and subtract
}The “count everything, then subtract per branch” pattern avoids the pairing and is the standard way to write it.
Alternatively, process branches one at a time, querying against an accumulator of all previous branches — no subtraction needed, and often cleaner:
map<int,int> seen; seen[0] = 1; // the centroid itself
for (int u : adj[c]) {
if (removed[u]) continue;
vector<int> cur; collect(u, c, 1, cur);
for (int d : cur) ans += seen.count(K - d) ? seen[K - d] : 0; // query first
for (int d : cur) seen[d]++; // then insert
}What it solves
| Problem | Per-centroid work |
|---|---|
| Count paths of length exactly | count depth pairs summing to |
| Count paths of length | sort depths, two pointers |
| Count paths with XOR | hash map of prefix XORs |
| Closest marked vertex to each query | store distances to ancestors in the centroid tree |
| Add to all vertices within distance | update along centroid ancestors |
| Count paths with at most “bad” edges | 2D counting with a BIT |
| Distance-limited queries with updates | the centroid tree as an index |
The centroid-tree-as-index technique
For each vertex , store its distance to each of its centroid ancestors. Then:
- Update ( becomes marked): walk up the centroid tree, updating each ancestor’s stored minimum with
dist(v, ancestor). - Query (nearest marked to ): walk up, taking over
dist(u, ancestor) + best[ancestor].
Both (plus the cost of the distance query). This solves the classic “paint a vertex / find the nearest painted vertex” problem, and it is the main reason to build the centroid tree explicitly rather than just recursing.
Distances to centroid ancestors are precomputed during the decomposition, or answered with LCA in .
Complexity
- Build:
- Memory: if you store all vertex-to-centroid-ancestor distances, otherwise
- Per query/update using the centroid tree: or
Centroid vs heavy-light
| Centroid decomposition | HLD | |
|---|---|---|
| Splits by | subtree size (vertices) | heavy paths (edges) |
| Structure | a tree of depth | chains per root-leaf path |
| Good for | counting over all paths, distance queries | path updates and queries between two given vertices |
| Typical query | ”how many pairs at distance " | "sum along the path ” |
They solve different problems and are not interchangeable.
See also: HLD · Tree Fundamentals · DSU on Tree