[模板] 树的重心/点分治/动态点分治
树的重心
定义
\((1)\) 树的重心 \(\leftrightarrow\) 所有的子树中最大的子树节点数最少的节点.
\((2)\) 树的重心 \(\leftrightarrow\) 所有子树的大小都不超过整个树大小的一半的节点.
性质
删去重心后, 生成的多棵树尽可能平衡.
一棵树中最少有两个重心.
树中所有点到某个点的距离和中, 到重心的距离和是最小的; 如果有两个重心, 那么他们的距离和一样.
把两个树通过一条边相连得到一个新的树, 那么新的树的重心在连接原来两个树的重心的路径上.
把一个树添加或删除一个叶子, 那么它的重心最多只移动一条边的距离.
求法&&代码
根据定义\((1)\), dfs求即可.
int szp[nsz],sum,maxp[nsz]{2e4+5},rt=0;
void getrt(int p,int fa){
szp[p]=1,maxp[p]=0;
forg(p,i,v){
if(v==fa)continue;
getrt(v,p);
szp[p]+=szp[v];
maxp[p]=max(maxp[p],szp[v]);
}
maxp[p]=max(maxp[p],sum-szp[p]);
if(maxp[p]<maxp[rt])rt=p;
}
点分治
简介
点分治可以解决一些树上的路径问题.
对于一棵无根树, 可以将一个点 \(rt\) 设为根, 把它转化为有根树. 那么树上的路径可以分为两类:
- 经过根的路径;
- 仅在某个子树内的路径.
对于1, 我们可以将链 \((a,b)\) 分为 \((a,rt)\), \((rt,b)\), 多数情况可以合并他们的影响 (例如求路径长度). 一般有两种做法:
-
处理 \(rt\) 及其所有子节点, 求出两两之间的对答案的贡献, 再减去同一子树内的点对对答案的贡献.
-
分别递归 \(rt\) 的每个子树, 对每个子树求出其与之前的子树中的节点形成点对的贡献. 这样就不需要容斥减去重复的部分.
对于2, 可以递归求解.
可以证明, 如果在递归每个子树时都将子树的重心设为根, 那么递归层数是 \(O(\log n)\) 的.
代码
//store the tree
const int nsz=20050;
ll n;
struct te{int t,pr,v;}edge[nsz*2];
int hd[nsz],pe=1;
void adde(int f,int t,int v){edge[++pe]=(te){t,hd[f],v};hd[f]=pe;}
void adddb(int f,int t,int v){adde(f,t,v);adde(t,f,v);}
#define forg(p,i,v) for(int i=hd[p],v=edge[i].t;i;i=edge[i].pr,v=edge[i].t)
//find center of tree
int vi[nsz];
int szp[nsz],sum,maxp[nsz]{2e4+5},rt;
void getrt(int p,int fa){
szp[p]=1,maxp[p]=0;
forg(p,i,v){
if(vi[v]||v==fa)continue;
getrt(v,p);
szp[p]+=szp[v];
maxp[p]=max(maxp[p],szp[v]);
}
maxp[p]=max(maxp[p],sum-szp[p]);
if(maxp[p]<maxp[rt])rt=p;
}
void sol1(int rt,int v0,int fl){
//do something
}
void divide(int p){
int sum0=sum;
vi[p]=1;
sol1(p,0,1);
forg(p,i,v){
if(vi[v])continue;
sol1(v,edge[i].v,-1);
rt=0,sum=sum0-maxp[v],getrt(v,0);
divide(rt);
}
}
ll sol(){
rt=0,sum=n,getrt(1,0); //init
divide(rt);
return ans;
}