一、什么是树的重心?
重心的定义:
对于树中的某个节点u:
-
删除u后,树会分裂成若干子树
-
记其中最大的子树大小为
max_subtree_size(u)
-
重心是使
max_subtree_size(u)
最小的节点
重心的性质:
-
一棵树至少有1个重心,最多2个重心
-
如果有两个重心,它们必定相邻
-
删除重心后,所有子树的大小 ≤ n/2(这是点分治效率的保证)
如何计算重心?
int sz[MAXN]; // 记录子树大小
int centroid = -1, min_max_sub = INT_MAX;
void dfs_centroid(int u, int fa, int total_nodes) {
sz[u] = 1;
int max_sub = 0;
for (int v : tree[u]) {
if (v == fa || vis[v]) continue;
dfs_centroid(v, u, total_nodes);
sz[u] += sz[v];
max_sub = max(max_sub, sz[v]); // 子节点方向的子树
}
max_sub = max(max_sub, total_nodes - sz[u]); // 父节点方向的"子树"
if (max_sub < min_max_sub) {
min_max_sub = max_sub;
centroid = u;
}
}
// 调用方式:dfs_centroid(root, -1, n);
二、点分治算法思路
寻找树的重心,将树分割成n颗大小近似相同的子树,操作每一颗子树,然后递归再次寻找每一颗子树的重心,从而再次操作每一颗子树······
每次递归问题规模至少减半,形成递归树高度为O(log n)
为什么选择重心?
因为重心能保证树被均衡分割,递归深度为O(log n),避免退化成链的最坏情况。
三、一个具体问题
题目描述
给定一棵有 n 个点的树,询问树上距离为 k 的点对是否存在。
输入格式
第一行两个数 n,m。
第 2 到第 n 行,每行三个整数 u,v,w,代表树上存在一条连接 u 和 v 边权为 w 的路径。
接下来 m 行,每行一个整数 k,代表一次询问。
输出格式
对于每次询问输出一行一个字符串代表答案,存在输出 AYE
,否则输出 NAY
。
io样例
输入 #1 输出#1
2 1 AYE 1 2 2 2
解决代码
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int maxn = 10010;
const int maxm = 110;
const int maxk = 10000000;
//边结构体
struct Edge{
int to, w;
Edge(int t, int weight) : to(t), w(weight){}
};
vector<Edge> G[maxn]; //邻接表存图
int n, m;
int tim = 0; //全局时间戳
int time_exist[maxk+1] = {0}; //记录距离的时间戳
int q[maxm+1]; //存储所有查询的k值
int sz[maxn+1]; //记录子树大小
bool vis[maxn+1] = {false}; //标记节点是否被处理
bool exist[maxk+1] = {false}; //记录距离是否存在
bool ans[maxm+1] = {false}; //存储每个查询的答案
//计算子树大小
void get_size(int u, int fa){
sz[u] = 1;
for(auto &e : G[u]){
int v = e.to;
if(v != fa && !vis[v]){
get_size(v, u);
sz[u] += sz[v];
}
}
}
//找树的重心
void get_root(int u, int fa, int total, int &root, int &min_size){
int max_part = 0;//删除重心后最大子树的大小
for(auto &e : G[u]){
int v = e.to;
if(v != fa && !vis[v]){
get_root(v, u, total, root, min_size);
max_part = max(max_part, sz[v]);
}
}
max_part = max(max_part, total - sz[u]);
if(max_part < min_size){// 找到更平衡的重心
root = u;
min_size = max_part;
}
}
void dfs_dis(int u, int fa, int dis, vector<int> &temp){
if(dis > maxk) return; //剪枝
temp.push_back(dis);
for(auto &e : G[u]){
int v = e.to, w = e.w;
if(v != fa && !vis[v]){
dfs_dis(v, u, dis+w, temp);
}
}
}
void solve(int u){
vis[u] = true;// 标记当前分治中心已处理
exist[0] = true;
tim++;
time_exist[0] = tim;
for(auto &e : G[u]){
int v = e.to, w = e.w;
if(vis[v]) continue;
vector<int> temp;
dfs_dis(v, u, w, temp);//计算子树每一个点到重心的距离
// 检查当前子树的距离能否与已记录距离配对
for(int d : temp){
for(int i = 0; i < m; i++){
if(ans[i]) continue;
int r = q[i] - d;
if(r < 0 || r > maxk) continue;
if(exist[r] && time_exist[r] == tim){
ans[i] = true;
}
}
}
// 记录当前子树的所有距离
for(int d : temp){
exist[d] = true;
time_exist[d] = tim;
}
}
for(auto &e : G[u]){
int v = e.to;
if(!vis[v]){
get_size(v, u);
int total = sz[v];
int root_v, min_size = n + 10;
get_root(v, 0, total, root_v, min_size);
solve(root_v);
}
}
}
int main (){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
for(int i = 1; i < n; i++){
int a, b, c;
cin >> a >> b >> c;
G[a].push_back(Edge(b, c));
G[b].push_back(Edge(a, c));
}
for(int i = 0; i < m; i++)
cin >> q[i];
int root, min_size = n + 10;
get_size(1, 0);
get_root(1, 0, n, root, min_size);
solve(root);
for(int i = 0; i < m; i++){
if(ans[i]) cout << "AYE\n";
else cout << "NAY\n";
}
return 0;
}
时间复杂度分析
-
找重心:O(n) 每次处理
-
递归深度:O(log n) 层
-
每层处理:所有节点O(n),每个节点处理m个查询
-
总复杂度:O(n log n × m)