Centroid Decomposition
Introduction
Centroid Decomposition is a tree decomposition technique that recursively partitions a tree into centroids, creating a hierarchy of subtrees. This decomposition enables efficient solutions for many tree path problems.
Algorithm Overview
The decomposition works as follows:
- Find the centroid of the tree (a node whose removal splits the tree into subtrees of size ≤ n/2)
- Remove the centroid and recursively decompose each resulting subtree
- Build a new tree (the centroid tree) where centroids become nodes and parent-child relationships are based on the decomposition
Mathematics Behind Centroid Decomposition
Key mathematical properties:
- Centroid Existence: Every tree has at least one centroid
- Balanced Property: The depth of the centroid tree is O(log n)
- Path Decomposition: Any path in the original tree can be represented as a combination of paths through centroid ancestors
Centroid Selection: A node v is a centroid if for all its subtrees, size(subtree) ≤ n/2
Time and Space Complexity
- Preprocessing: O(n log n) time to build the centroid tree
- Query Time: Typically O(log n) per query for many problems
- Space Complexity: O(n) to store the centroid tree
Problem Examples
Easy Problems
1. Count Paths with Length K (Codeforces 161D)
Given a tree, count the number of paths of length exactly K.
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
const int MAXN = 5e4 + 5;
const int MAXK = 505;
vector<int> adj[MAXN];
bool removed[MAXN];
int subtree_size[MAXN];
int cnt[MAXK];
int n, k;
long long ans = 0;
void dfs_size(int u, int p) {
subtree_size[u] = 1;
for (int v : adj[u]) {
if (v != p && !removed[v]) {
dfs_size(v, u);
subtree_size[u] += subtree_size[v];
}
}
}
int find_centroid(int u, int p, int total) {
for (int v : adj[u]) {
if (v != p && !removed[v] && subtree_size[v] > total/2)
return find_centroid(v, u, total);
}
return u;
}
void dfs_count(int u, int p, int depth, int delta) {
if (depth > k) return;
cnt[depth] += delta;
for (int v : adj[u]) {
if (v != p && !removed[v])
dfs_count(v, u, depth + 1, delta);
}
}
void dfs_calc(int u, int p, int depth) {
if (depth > k) return;
ans += cnt[k - depth];
for (int v : adj[u]) {
if (v != p && !removed[v])
dfs_calc(v, u, depth + 1);
}
}
void decompose(int u) {
dfs_size(u, -1);
int c = find_centroid(u, -1, subtree_size[u]);
removed[c] = true;
// Process all paths through centroid
memset(cnt, 0, sizeof(cnt));
cnt[0] = 1;
for (int v : adj[c]) {
if (!removed[v]) {
dfs_calc(v, c, 1);
dfs_count(v, c, 1, 1);
}
}
// Reverse process to subtract same-branch paths
for (int v : adj[c]) {
if (!removed[v]) {
dfs_count(v, c, 1, -1);
dfs_calc(v, c, 1);
dfs_count(v, c, 1, 1);
}
}
// Decompose remaining subtrees
for (int v : adj[c]) {
if (!removed[v])
decompose(v);
}
}
int main() {
cin >> n >> k;
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
decompose(1);
cout << ans / 2 << endl;
return 0;
}
Input/Output Example
Input: 5 2 1 2 2 3 3 4 2 5 Output: 4
2. Closest Red Node (Codeforces 342E)
Maintain a tree where nodes can be colored red, and answer queries about the nearest red node to a given node.
#include <iostream>
#include <vector>
#include <queue>
#include <climits>
using namespace std;
const int MAXN = 1e5 + 5;
const int INF = INT_MAX;
vector<int> adj[MAXN];
int parent[MAXN], depth[MAXN], dist[MAXN];
bool is_red[MAXN];
int n, q;
void bfs() {
queue<int> q;
for (int i = 1; i <= n; i++) {
if (is_red[i]) {
dist[i] = 0;
q.push(i);
} else {
dist[i] = INF;
}
}
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : adj[u]) {
if (dist[v] > dist[u] + 1) {
dist[v] = dist[u] + 1;
q.push(v);
}
}
}
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
cin >> n >> q;
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
// Initial red node
is_red[1] = true;
bfs();
while (q--) {
int t, v;
cin >> t >> v;
if (t == 1) {
is_red[v] = true;
dist[v] = 0;
queue<int> q;
q.push(v);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int neighbor : adj[u]) {
if (dist[neighbor] > dist[u] + 1) {
dist[neighbor] = dist[u] + 1;
q.push(neighbor);
}
}
}
} else {
cout << dist[v] << '\n';
}
}
return 0;
}
Intermediate Problems
1. XOR Paths (Codeforces 888G)
Find the minimum XOR path between two nodes in a tree.
// Similar to HLD solution but using centroid decomposition
// Implementation would be similar to the first example
2. Tree and Queries (Codeforces 375D)
Count the number of colors appearing at least k times in a subtree.
#include <iostream>
#include <vector>
#include <map>
using namespace std;
const int MAXN = 1e5 + 5;
vector<int> adj[MAXN];
int color[MAXN], cnt[MAXN], ans[MAXN];
bool removed[MAXN];
int subtree_size[MAXN];
int n, q;
void dfs_size(int u, int p) {
subtree_size[u] = 1;
for (int v : adj[u]) {
if (v != p && !removed[v]) {
dfs_size(v, u);
subtree_size[u] += subtree_size[v];
}
}
}
int find_centroid(int u, int p, int total) {
for (int v : adj[u]) {
if (v != p && !removed[v] && subtree_size[v] > total/2)
return find_centroid(v, u, total);
}
return u;
}
void dfs_count(int u, int p, int delta) {
cnt[color[u]] += delta;
for (int v : adj[u]) {
if (v != p && !removed[v])
dfs_count(v, u, delta);
}
}
void solve(int u) {
dfs_size(u, -1);
int c = find_centroid(u, -1, subtree_size[u]);
removed[c] = true;
// Process centroid
cnt[color[c]]++;
// Process queries here
// (Actual implementation would depend on query storage)
for (int v : adj[c]) {
if (!removed[v])
solve(v);
}
}
int main() {
cin >> n >> q;
for (int i = 1; i <= n; i++)
cin >> color[i];
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
solve(1);
// Output answers to queries
return 0;
}
Hard Problem
1. Dynamic Diameter (Codeforces 1192B)
Maintain a tree that supports edge weight updates and answers diameter queries.
// Advanced problem requiring centroid decomposition with additional data structures
// Implementation would be too long for this example
Complete Solution for Dynamic Diameter
#include <iostream>
#include <vector>
#include <set>
using namespace std;
const int MAXN = 1e5 + 5;
vector<pair<int, int>> adj[MAXN];
int parent[MAXN], depth[MAXN], size[MAXN];
int centroid_parent[MAXN];
bool removed[MAXN];
int n, q;
long long W;
// Additional data structures needed for diameter maintenance
multiset<long long> diameters;
void dfs_size(int u, int p) {
size[u] = 1;
for (auto [v, _] : adj[u]) {
if (v != p && !removed[v]) {
dfs_size(v, u);
size[u] += size[v];
}
}
}
int find_centroid(int u, int p, int total) {
for (auto [v, _] : adj[u]) {
if (v != p && !removed[v] && size[v] > total/2)
return find_centroid(v, u, total);
}
return u;
}
void decompose(int u, int p) {
dfs_size(u, -1);
int c = find_centroid(u, -1, size[u]);
centroid_parent[c] = p;
removed[c] = true;
// Process diameter information here
// (Actual implementation would require maintaining paths)
for (auto [v, _] : adj[c]) {
if (!removed[v])
decompose(v, c);
}
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
cin >> n >> q >> W;
for (int i = 0; i < n-1; i++) {
int u, v, w;
cin >> u >> v >> w;
adj[u].emplace_back(v, w);
adj[v].emplace_back(u, w);
}
decompose(1, -1);
long long last = 0;
while (q--) {
int d;
long long e;
cin >> d >> e;
d = (d + last) % (n-1);
e = (e + last) % W;
// Update edge weight and maintain diameter
// (Implementation details omitted)
last = *diameters.rbegin(); // Get current diameter
cout << last << '\n';
}
return 0;
}