树心分解算法详解与模板
树心分解算法详解与模板
树心分解(点分治)是一种处理树上路径问题的经典算法。其核心思想是通过递归找到树的中心(重心),将树分割成若干子树,然后分别处理跨越不同子树的路径。这种分治策略可以将复杂度从 O(n²) 优化到 O(n log n)。
算法核心步骤
- 寻找重心:在当前子树中找到一个节点,使得删除该节点后剩余子树的最大规模最小
- 处理路径:统计以重心为中介的所有路径情况
- 递归处理:对重心之外的子树递归执行相同操作
例题一:统计满足距离条件的节点对
给定一棵带权树,统计有多少对节点之间的距离不超过给定值 m。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 100005;
int nodeCount, limitValue;
int head[MAXN], to[MAXN << 1], nxt[MAXN << 1], edgeWeight[MAXN << 1], edgeCnt;
int tempArr[MAXN], collectArr[MAXN];
bool removed[MAXN];
void addEdge(int u, int v, int w) {
to[++edgeCnt] = v;
edgeWeight[edgeCnt] = w;
nxt[edgeCnt] = head[u];
head[u] = edgeCnt;
}
int getSize(int u, int parent) {
if (removed[u]) return 0;
int result = 1;
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if (v == parent) continue;
result += getSize(v, u);
}
return result;
}
int findCentroid(int u, int parent, int total, int ¢roid) {
if (removed[u]) return 0;
int subSize = 1, maxPart = 0;
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if (v == parent) continue;
int childSize = findCentroid(v, u, total, centroid);
maxPart = max(maxPart, childSize);
subSize += childSize;
}
maxPart = max(maxPart, total - subSize);
if (maxPart <= total / 2) centroid = u;
return subSize;
}
void collectDist(int u, int parent, int dist) {
if (removed[u]) return;
tempArr[++collectArr[0]] = dist;
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if (v == parent) continue;
collectDist(v, u, dist + edgeWeight[i]);
}
}
int countPairs(int arr[], int len) {
sort(arr + 1, arr + len + 1);
int answer = 0;
for (int i = len, j = 0; i >= 1; i--) {
while (j + 1 < i && arr[j + 1] + arr[i] <= limitValue) j++;
j = min(j, i - 1);
answer += j;
}
return answer;
}
int solve(int u) {
if (removed[u]) return 0;
int result = 0, tempCnt = 0;
findCentroid(u, -1, getSize(u, -1), u);
removed[u] = true;
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
collectArr[0] = 0;
collectDist(v, -1, edgeWeight[i]);
result -= countPairs(tempArr, collectArr[0]);
for (int k = 1; k <= collectArr[0]; k++) {
if (tempArr[k] <= limitValue) result++;
collectArr[++tempCnt] = tempArr[k];
}
}
result += countPairs(collectArr, tempCnt);
for (int i = head[u]; i; i = nxt[i])
result += solve(to[i]);
return result;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
while (cin >> nodeCount >> limitValue, nodeCount || limitValue) {
memset(head, 0, sizeof(head));
memset(removed, 0, sizeof(removed));
edgeCnt = 0;
for (int i = 1, u, v, w; i < nodeCount; i++) {
cin >> u >> v >> w;
addEdge(u, v, w);
addEdge(v, u, w);
}
cout << solve(0) << endl;
}
return 0;
}
例题二:IOI 2011 Race - 寻找最短路径
给定一棵带权树和一个目标距离 m,求包含恰好 m 长度路径的最小边数。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
const int INF = 0x3f3f3f3f;
int nodeCount, targetDist;
int head[MAXN], to[MAXN << 1], nxt[MAXN << 1], weight[MAXN << 1], edgeCnt;
int collectArr[MAXN];
int distInfo[1000005];
int answer = INF;
pair<int, int> storeArr[MAXN], tempArr[MAXN];
bool removed[MAXN];
void addEdge(int u, int v, int w) {
to[++edgeCnt] = v;
weight[edgeCnt] = w;
nxt[edgeCnt] = head[u];
head[u] = edgeCnt;
}
int getSize(int u, int parent) {
if (removed[u]) return 0;
int result = 1;
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if (v == parent) continue;
result += getSize(v, u);
}
return result;
}
int findCentroid(int u, int parent, int total, int ¢roid) {
if (removed[u]) return 0;
int subSize = 1, maxPart = 0;
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if (v == parent) continue;
int childSize = findCentroid(v, u, total, centroid);
maxPart = max(maxPart, childSize);
subSize += childSize;
}
maxPart = max(maxPart, total - subSize);
if (maxPart <= total / 2) centroid = u;
return subSize;
}
void collectInfo(int u, int parent, int dist, int edges) {
if (removed[u] || dist > targetDist) return;
tempArr[++collectArr[0]] = {dist, edges};
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if (v == parent) continue;
collectInfo(v, u, dist + weight[i], edges + 1);
}
}
void decompose(int u) {
if (removed[u]) return;
int tempCnt = 0;
findCentroid(u, -1, getSize(u, -1), u);
removed[u] = true;
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
collectArr[0] = 0;
collectInfo(v, -1, weight[i], 1);
for (int k = 1; k <= collectArr[0]; k++) {
auto &item = tempArr[k];
if (item.first == targetDist)
answer = min(answer, item.second);
answer = min(answer, distInfo[targetDist - item.first] + item.second);
storeArr[++tempCnt] = item;
}
for (int k = 1; k <= collectArr[0]; k++) {
auto &item = tempArr[k];
distInfo[item.first] = min(distInfo[item.first], item.second);
}
}
for (int i = 1; i <= tempCnt; i++)
distInfo[storeArr[i].first] = INF;
for (int i = head[u]; i; i = nxt[i])
decompose(to[i]);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> nodeCount >> targetDist;
for (int i = 1, u, v, w; i < nodeCount; i++) {
cin >> u >> v >> w;
addEdge(u, v, w);
addEdge(v, u, w);
}
memset(distInfo, 0x3f, sizeof(distInfo));
decompose(0);
cout << (answer == INF ? -1 : answer) << endl;
return 0;
}
例题三:点分治模板 - 多查询距离存在性
给定一棵带权树和 q 个查询,对于每个查询判断是否存在两点间的距离等于该查询值。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10005;
const int MAXE = MAXN * 2;
int head[MAXN], to[MAXE], nxt[MAXE], weight[MAXE], edgeCnt;
int nodeNum, queryNum, queryVals[105];
int root, subtreeSize[MAXN], maxChild[MAXN], totalSize;
int depthArr[MAXN], distArr[MAXN], depthCnt;
bool visited[MAXN], resultArr[MAXN], existFlag[10000005];
vector<int> usedDists;
void addEdge(int u, int v, int w) {
to[++edgeCnt] = v;
weight[edgeCnt] = w;
nxt[edgeCnt] = head[u];
head[u] = edgeCnt;
}
void computeRoot(int u, int parent) {
subtreeSize[u] = 1;
maxChild[u] = 0;
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if (v == parent || visited[v]) continue;
computeRoot(v, u);
subtreeSize[u] += subtreeSize[v];
maxChild[u] = max(maxChild[u], subtreeSize[v]);
}
maxChild[u] = max(maxChild[u], totalSize - subtreeSize[u]);
root = (maxChild[root] < maxChild[u] ? root : u);
}
void getDistances(int u, int parent) {
depthArr[++depthCnt] = distArr[u];
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if (v == parent || visited[v]) continue;
distArr[v] = distArr[u] + weight[i];
getDistances(v, u);
}
}
void process(int center) {
usedDists.clear();
existFlag[0] = true;
for (int i = head[center]; i; i = nxt[i]) {
int v = to[i];
if (visited[v]) continue;
distArr[v] = weight[i];
depthCnt = 0;
getDistances(v, 0);
for (int j = 1; j <= depthCnt; j++) {
for (int k = 1; k <= queryNum; k++) {
if (queryVals[k] < depthArr[j]) continue;
resultArr[k] |= existFlag[queryVals[k] - depthArr[j]];
}
}
for (int j = 1; j <= depthCnt; j++) {
if (depthArr[j] > 1e7) continue;
if (!existFlag[depthArr[j]]) {
existFlag[depthArr[j]] = true;
usedDists.push_back(depthArr[j]);
}
}
}
for (int i = 0; i < usedDists.size(); i++)
existFlag[usedDists[i]] = false;
}
void decompose(int u) {
visited[u] = true;
process(u);
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if (visited[v]) continue;
maxChild[root = 0] = totalSize = subtreeSize[v];
computeRoot(v, 0);
decompose(root);
}
}
int main() {
cin >> nodeNum >> queryNum;
for (int i = 1, x, y, z; i < nodeNum; i++) {
cin >> x >> y >> z;
addEdge(x, y, z);
addEdge(y, x, z);
}
for (int i = 1; i <= queryNum; i++) cin >> queryVals[i];
maxChild[root] = totalSize = nodeNum;
computeRoot(1, 0);
decompose(root);
for (int i = 1; i <= queryNum; i++)
cout << (resultArr[i] ? "AYE" : "NAY") << endl;
return 0;
}
算法复杂度分析
树心分解的核心在于每次递归都将树的大小减半,因此整体时间复杂度为 O(n log n)。在处理每个重心时,需要遍历所有子树并收集距离信息,这部分通常配合排序或哈希表来实现。空间复杂度主要为 O(n)。
该算法适用于处理树上路径统计、距离查询、路径权值计算等多种问题,是竞赛中必备的知识点。