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

ProblemPer-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 querystore distances to ancestors in the centroid tree
Add to all vertices within distance update along centroid ancestors
Count paths with at most “bad” edges2D counting with a BIT
Distance-limited queries with updatesthe 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 decompositionHLD
Splits bysubtree size (vertices)heavy paths (edges)
Structurea tree of depth chains per root-leaf path
Good forcounting over all paths, distance queriespath 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