树分治 学习笔记
定义
树分治通过分治降低遍历树上路径的复杂度。适用于一些需要遍历树上所有路径的问题。
点分治
过程
钦定某个点为根,对于经过这个点的路径需要有一个快速处理的方式。不经过这个点的路径一定在某一棵子树上,可以删去这个点,对子树递归处理子问题。
为了使子问题的规模尽量小,钦定的这个点应当选择重心,因为重心的性质使得子问题的规模每次减半,所以是对数级别的。
例题
点分治主体部分。
void dfs(int pos){
vis[pos]=b[0]=1,calc(pos,0);
for(int i=0;i<e[pos].size();i++)if(!vis[e[pos][i].first])rt=0,sum=sz[e[pos][i].first],dfs1(e[pos][i].first,0),dfs(rt);
}
求重心的函数。根据定义,重心是最大子树最小的节点。
void dfs1(int pos,int fa){
maxn[pos]=0,sz[pos]=1;
for(int i=0;i<e[pos].size();i++){
if(vis[e[pos][i].first]||e[pos][i].first==fa)continue;
dfs1(e[pos][i].first,pos),sz[pos]+=sz[e[pos][i].first],maxn[pos]=max(maxn[pos],sz[e[pos][i].first]);
}
maxn[pos]=max(maxn[pos],sum-sz[pos]);
if(maxn[pos]<maxn[rt])rt=pos;
}
其中 \(maxn,size,sum,rt,vis\) 分别为最大子树大小、子树大小、当前树的大小、重心、是否被删去,调用前需要初始化。
统计答案的函数。把每条路径拆成 \(u\) 到重心和重心到 \(v\),把重心到所有节点的距离存起来,问题变成这些数能否凑出询问的值。
把询问离线,枚举子树,把之前的子树的距离扔进桶,如果询问的值减去当前子树某节点的距离在桶里存在,那么答案为真。
void dfs2(int pos,int fa,int d){
dep[++cnt]=d;
for(int i=0;i<e[pos].size();i++){
if(vis[e[pos][i].first]||e[pos][i].first==fa)continue;
dfs2(e[pos][i].first,pos,d+e[pos][i].second);
}
}
void calc(int pos,int d){
num=0;
for(int i=0;i<e[pos].size();i++){
if(!vis[e[pos][i].first]){
cnt=0,dfs2(e[pos][i].first,pos,e[pos][i].second);
for(int j=1;j<=cnt;j++)for(int k=1;k<=m;k++)if(q[k]>=dep[j])ans[k]|=b[q[k]-dep[j]];
for(int j=1;j<=cnt;j++)if(dep[j]<=10000000)temp[++num]=dep[j],b[dep[j]]=1;
}
}
for(int i=1;i<=num;i++)b[temp[i]]=0;
}
完整代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,q[105],sum,rt,maxn[10005],sz[10005],dep[10005],cnt,temp[10005],num;
bool vis[10005],ans[105],b[10000005];
vector<pair<int,int> >e[10005];
void dfs1(int pos,int fa){
maxn[pos]=0,sz[pos]=1;
for(int i=0;i<e[pos].size();i++){
if(vis[e[pos][i].first]||e[pos][i].first==fa)continue;
dfs1(e[pos][i].first,pos),sz[pos]+=sz[e[pos][i].first],maxn[pos]=max(maxn[pos],sz[e[pos][i].first]);
}
maxn[pos]=max(maxn[pos],sum-sz[pos]);
if(maxn[pos]<maxn[rt])rt=pos;
}
void dfs2(int pos,int fa,int d){
dep[++cnt]=d;
for(int i=0;i<e[pos].size();i++){
if(vis[e[pos][i].first]||e[pos][i].first==fa)continue;
dfs2(e[pos][i].first,pos,d+e[pos][i].second);
}
}
void calc(int pos,int d){
num=0;
for(int i=0;i<e[pos].size();i++){
if(!vis[e[pos][i].first]){
cnt=0,dfs2(e[pos][i].first,pos,e[pos][i].second);
for(int j=1;j<=cnt;j++)for(int k=1;k<=m;k++)if(q[k]>=dep[j])ans[k]|=b[q[k]-dep[j]];
for(int j=1;j<=cnt;j++)if(dep[j]<=10000000)temp[++num]=dep[j],b[dep[j]]=1;
}
}
for(int i=1;i<=num;i++)b[temp[i]]=0;
}
void dfs(int pos){
vis[pos]=b[0]=1,calc(pos,0);
for(int i=0;i<e[pos].size();i++)if(!vis[e[pos][i].first])rt=0,sum=sz[e[pos][i].first],dfs1(e[pos][i].first,0),dfs(rt);
}
int main(){
cin>>n>>m;
for(int i=1,u,v,w;i<n;i++)cin>>u>>v>>w,e[u].push_back(make_pair(v,w)),e[v].push_back(make_pair(u,w));
for(int i=1;i<=m;i++)cin>>q[i];
sum=maxn[0]=n,dfs1(1,0),dfs(rt);
for(int i=1;i<=m;i++)puts(ans[i]?"AYE":"NAY");
return 0;
}
统计答案时,将所有距离排序,问题变成统计这些数里和小于 \(k\) 的个数,用双指针可以解决。
int calc(int pos,int d,int ans=0){
cnt=0,dfs2(pos,0,d),sort(dep+1,dep+cnt+1);
for(int l=1,r=cnt;l<r;){
if(dep[l]+dep[r]<=k)ans+=r-l,l++;
else r--;
}
return ans;
}
这样做会有一个问题,两个节点会被 LCA 的祖先都统计。
因此在统计答案时需要去掉相同子树内节点的贡献。如果蓝色被统计,那么红色在递归到以 \(2\) 为根的子树时也会被统计,因此需要减去。
void dfs(int pos){
vis[pos]=1,ans+=calc(pos,0);
for(int i=0;i<e[pos].size();i++){
if(!vis[e[pos][i].first]){
ans-=calc(e[pos][i].first,e[pos][i].second);
rt=0,sum=sz[e[pos][i].first],dfs1(e[pos][i].first,0),dfs(rt);
}
}
}
边分治
从边的角度考虑,每次选出一条中心边,使得边两侧的大小尽量相等。
然而这会有一个问题:构造一个菊花图,每次问题的规模只减少一,直接 T 飞。
这时需要对树三度化,转成二叉树。对于一个节点有多个儿子的情况,可以添加虚点组成一条链下挂这些儿子。此时新树需要开两倍空间。
void rebuild(int pos,int fa){
for(int i=0,p=0;i<g[pos].size();i++){
if(g[pos][i].first==fa)continue;
if(!p)add(pos,g[pos][i].first,g[pos][i].second),add(g[pos][i].first,pos,g[pos][i].second),p=pos;
else n++,add(p,n,0),add(n,p,0),add(n,g[pos][i].first,g[pos][i].second),add(g[pos][i].first,n,g[pos][i].second),p=n;
rebuild(g[pos][i].first,pos);
}
}
几乎所有点分治的题都可以用边分治做。边分治相比点分治,合并子树答案时只需要合并一次,缺点是虚点不能影响答案。
[[树论]]

浙公网安备 33010602011771号