树分治 学习笔记

定义

树分治通过分治降低遍历树上路径的复杂度。适用于一些需要遍历树上所有路径的问题。

点分治

过程

钦定某个点为根,对于经过这个点的路径需要有一个快速处理的方式。不经过这个点的路径一定在某一棵子树上,可以删去这个点,对子树递归处理子问题。

为了使子问题的规模尽量小,钦定的这个点应当选择重心,因为重心的性质使得子问题的规模每次减半,所以是对数级别的。

例题

P3806

点分治主体部分。

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;
}

P4178

统计答案时,将所有距离排序,问题变成统计这些数里和小于 \(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);
  }
}

几乎所有点分治的题都可以用边分治做。边分治相比点分治,合并子树答案时只需要合并一次,缺点是虚点不能影响答案。

[[树论]]

posted @ 2024-03-01 09:26  lgh_2009  阅读(11)  评论(0)    收藏  举报