树分治全家桶
树分治全家桶
树,(是一种益于保护环境植物)是图论当中的一种特殊图,由于(绿化环境的作用非常优秀)特殊性质丰富,经常出现在我们身边。
本文将主要介绍(如何植树)一种树上优美的暴力——树分治。
树分治
树分治可以将部分暴力降至
Part1 点分治
点分治作为树分治的基础思想,主要利用树的重心进行不断划分。
例1:P3806 【模板】点分治 1
暴力枚举起点处理每一个询问,复杂度
从树的重心入手,找出重心(如果有多个任选其一)。

上图的树中,
以
现在考虑一条穿过
处理询问时,我们枚举一棵子树内所有路径长度
现在,所有穿过
图变为:

现在形成了森林,对于每一棵新树,我们分别求出树的重心,他们是
删除第二层分治中心,对于新森林重复上述操作。依次做下去直到删除完最后的节点,此时原树的路径都被且仅被一个分治中心考虑到,正确性显然。
分析时间复杂度,每层的分治中心所遍历的节点的总和是
例题代码:
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e4+5;
struct Edge
{
int tot;
int head[maxn];
struct edgenode{int to,nxt,w;}edge[maxn*2];
inline void add(int x,int y,int z)
{
tot++;
edge[tot].to=y;
edge[tot].w=z;
edge[tot].nxt=head[x];
head[x]=tot;
}
}T;
int n,m,tot,rt,crem;
int dis[maxn],q[maxn],rem[maxn],mx[maxn],qry[maxn];
bool jug[maxn*maxn],ans[maxn];
int siz[maxn];
bool book[maxn],cut[maxn];
inline void dfs_siz(int u)//求出以 u 为根的子树大小
{
book[u]=true;siz[u]=1;
for(int i=T.head[u];i;i=T.edge[i].nxt)
{
int v=T.edge[i].to;
if(book[v]||cut[v]) continue;
dfs_siz(v);siz[u]+=siz[v];
}
book[u]=false;
}
inline int dfs_rt(int u,const int tot)//返回重心,tot 为当前树的节点总个数
{
book[u]=true;int ret=u;
for(int i=T.head[u];i;i=T.edge[i].nxt)
{
int v=T.edge[i].to;
if(book[v]||cut[v]) continue;
if(siz[v]*2>=tot){ret=dfs_rt(v,tot);break;}
}
book[u]=false;return ret;
}
inline void dfs_dis(int u)//求出重心到 u 的距离,并记录在数组 rem 中
{
book[u]=true;
rem[++crem]=dis[u];
for(int i=T.head[u];i;i=T.edge[i].nxt)
{
int v=T.edge[i].to;
if(cut[v]||book[v]) continue;
dis[v]=dis[u]+T.edge[i].w;
dfs_dis(v);
}
book[u]=false;
}
inline void calc(int u)//考虑穿过重心的路径
{
queue<int>que;
while(!que.empty()) que.pop();
for(int i=T.head[u];i;i=T.edge[i].nxt)//枚举子树
{
int v=T.edge[i].to;
if(cut[v]) continue;
crem=0,dis[v]=T.edge[i].w;
dfs_dis(v);
for(int j=1;j<=crem;j++)//枚举路径长度
for(int k=1;k<=m;k++)//枚举询问
{
if(qry[k]>=rem[j]) ans[k]|=jug[qry[k]-rem[j]];
//查询其它子树是否存在长为 qry[k]-rem[j] 的路径
}
for(int j=1;j<=crem;j++) que.push(rem[j]),jug[rem[j]]=1;//标记存在一棵子树有长为 rem[j] 的路径
}
while(!que.empty()) jug[que.front()]=0,que.pop();
}
inline void dfs(int u)//处理 u 所在的新树
{
dfs_siz(u);int g=dfs_rt(u,siz[u]);cut[g]=1;//将重心 g 删除(标记为 1)
jug[0]=1;//g->g 的路径,方便与其他路径相加形成 g->x 的路径
calc(g);
for(int i=T.head[g];i;i=T.edge[i].nxt)
{
int v=T.edge[i].to;
if(cut[v]) continue;
dfs(v);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<n;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
T.add(x,y,z);
T.add(y,x,z);
}
for(int i=1;i<=m;i++) scanf("%d",&qry[i]);
dfs(1);
for(int i=1;i<=m;i++)
{
if(ans[i]) printf("AYE\n");
else printf("NAY\n");
}
}
总结
点分治本质是将图进行分治后固定分治中心跑单点的暴力,然后合并不同路径的答案,通常运用枚举子树的方式去重。
当然,这种算法并不支持在线处理。
Part2 边分治
边分治与点分治类似,不过是冷门算法。
选择一条边,这条边分成两边的树的大小最均匀,分别暴力处理两边树的信息,在通过边把所得的信息链接起来。
但如果图是菊花图的话边分治会被卡成
咋办哩?
把原树转成一棵二叉树,像这样:
Ps:图片引自 OI-wiki 树分治章节。
转成二叉树后消除了菊花图的弊端,使得边分治具有普遍性。
由于至多增加
点分治的题大部分边分治也可以做。
Part3 点分树(动态树分治)
树分治的终极版本,改变原树形态使得层数变为
点分治时,我们会删除该分治中心,形成了若干棵子树,将该分治中心与这些子树的分治中心连边,形成了一棵重构树。
根据点分治的时间复杂度证明,这棵树的深度小于
下面是原树改为重构树后的示例。
原树:

树分树后的重构树:

它牺牲了原来的结构来换取高速的结构,所以如果子树和父节点有路径(改变距离、一次性更改子树内所有节点、断开边、重连边……)关系,它就完了。
所以点分树和原树的常见性质是:
- 点分树内的一棵子树是原树的一个联通块。
- 点分树上两点的
在原树两点的路径上。 - 一个分治中心除了原树向上的那棵子树,其他子树两两的
为自己本身。
在点分树上有很多反直觉的地方:
- 点分树上的点对
间在原树上的距离与其在点分树上的距离无关(特别注意与祖先的原树距离也与点分树上的距离无关)。 - 点分树上的祖先、兄弟关系均与原树无关。
它最重要的是支持在线更改部分信息!
那这种树可以干什么呢?
例2:P6329 【模板】点分树 | 震波
暴力而言,遍历周围距离不超过
下文无特殊说明树均为点分树,每一个树上的节点
形式化的,
对于
将
一个简单的想法是,每次在
那么我们记录
形式化的,
在
现在将查询范围
修改一个点
使用动态开点线段树或
#include<bits/stdc++.h>
using namespace std;
#define inf 1e8
const int maxn=1e5+5;
struct Edge
{
int tot;
int head[maxn];
struct edgenode{int to,nxt;}edge[maxn*2];
inline void add(int x,int y)
{
tot++;
edge[tot].to=y;
edge[tot].nxt=head[x];
head[x]=tot;
}
}T;//原树边
struct Tree//线段树
{
int ct;
int rt[maxn];
struct node{int ch[2],val;}tree[maxn*55];
void insert(int &p,int l,int r,int x,int y)
{
if(!p) p=++ct;
if(l==r)
{
tree[p].val+=y;
return ;
}
int mid=(l+r)>>1;
if(x<=mid) insert(tree[p].ch[0],l,mid,x,y);
else insert(tree[p].ch[1],mid+1,r,x,y);
tree[p].val=tree[tree[p].ch[0]].val+tree[tree[p].ch[1]].val;
}
int query(int p,int l,int r,int ql,int qr)
{
if(ql>qr) return 0;
if(!p) return 0;
if(ql<=l&&r<=qr) return tree[p].val;
if(l>qr||r<ql) return 0;
int mid=(l+r)>>1;
return query(tree[p].ch[0],l,mid,ql,qr)+query(tree[p].ch[1],mid+1,r,ql,qr);
}
}w[2];//w[0] 同上文 w_0,w[1] 同上文 w
int n,tot,sum,m;
int val[maxn],siz[maxn],dis[maxn][25],fa[maxn],K[maxn];
//此处的 K 记录了 u 处在第几层,dis[u][i] 为 u 与第 i 层的祖先的距离
//使用 O(1) lca 或 vector<pair> 记录会获得更优美的实现
vector<int>E[maxn];
//记录点分树上的边
bool cut[maxn],book[maxn];
inline void dfs_siz(int u)
{
book[u]=true;siz[u]=1;
for(int i=T.head[u];i;i=T.edge[i].nxt)
{
int v=T.edge[i].to;
if(book[v]||cut[v]) continue;
dfs_siz(v);siz[u]+=siz[v];
}
book[u]=false;
}
inline int dfs_rt(int u,const int tot)
{
book[u]=true;int ret=u;
for(int i=T.head[u];i;i=T.edge[i].nxt)
{
int v=T.edge[i].to;
if(book[v]||cut[v]) continue;
if(siz[v]*2>=tot){ret=dfs_rt(v,tot);break;}
}
book[u]=false;return ret;
}
inline void dfs_dis(int u,int k)//求点分树上的第 k 层父亲与 u 的距离
{
book[u]=true;
for(int i=T.head[u];i;i=T.edge[i].nxt)
{
int v=T.edge[i].to;
if(cut[v]||book[v]) continue;
dis[v][k]=dis[u][k]+1;
dfs_dis(v,k);
}
book[u]=false;
}
inline void dfs_calc(int u,int op,int st,int k)//将贡献加入 w[0] 与 w[1]
{
book[u]=true;
w[op].insert(w[op].rt[st],0,inf,dis[u][k],val[u]);
for(int i=T.head[u];i;i=T.edge[i].nxt)
{
int v=T.edge[i].to;
if(cut[v]||book[v]) continue;
dfs_calc(v,op,st,k);
}
book[u]=false;
}
inline void dfs(int u,int f)
{
dfs_siz(u);int g=dfs_rt(u,siz[u]);cut[g]=true;
if(f) dfs_calc(g,0,g,K[f]);
E[f].push_back(g);fa[g]=f;
K[g]=K[fa[g]]+1;
dfs_dis(g,K[g]);
dfs_calc(g,1,g,K[g]);
for(int i=T.head[g];i;i=T.edge[i].nxt)
{
int v=T.edge[i].to;
if(cut[v]) continue;
dfs(v,g);
}
}
void change(int u,int st,int now)//修改
{
if(!u) return ;
w[1].insert(w[1].rt[u],0,inf,dis[st][K[u]],now-val[st]);//修改 w
if(fa[u]) w[0].insert(w[0].rt[u],0,inf,dis[st][K[u]-1],now-val[st]);//修改 w[0]
change(fa[u],st,now);//向上跳
}
int dfs_ans(int u,int v,int st,int d)
{
if(!u) return 0;
if(d-dis[st][K[u]]<0) return dfs_ans(fa[u],u,st,d);//此处距离超过查询范围,向上跳
int res=w[1].query(w[1].rt[u],0,inf,0,d-dis[st][K[u]]);//查询
if(v) res-=w[0].query(w[0].rt[v],0,inf,0,d-dis[st][K[u]]);//去重
return res+dfs_ans(fa[u],u,st,d);//跳父亲
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&val[i]);
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
T.add(u,v);
T.add(v,u);
}
dfs(1,0);
int ans=0;
for(int i=1;i<=m;i++)
{
int op,x,y;
scanf("%d%d%d",&op,&x,&y);
x^=ans,y^=ans;
if(op)
{
change(x,x,y);
val[x]=y;
}
else
{
printf("%d\n",ans=dfs_ans(x,0,x,y));
}
}
}
小结
点分树处理的问题多是带修的或强制在线的,通常与带
若本题不带修,将询问离线至点,使用朴素的前缀和(每次连通块的大小缩小
例3:ZJOI2007 捉迷藏
建处点分树,每个节点用 set
存与子树内黑点的距离,并将两个黑点的距离相加求出穿过该节点的最长黑点距离,全局用一个 set
维护这个信息。
对于一个修改,除了改变每个节点的 set
还改变将改变全局的 set
,从
时间复杂度
例4:P3345 ZJOI2015 幻想乡战略游戏
这道题是一道点分树的好题,巧妙的运用了点分树与原树的性质,利于更深刻的理解树分治。
同时使用了一个重要的技巧:点分树上二分查找关键点。
证明补给点是树的带权重心,即该删除该节点后,分出来的若干连通块没有一个点的权值和超过总权值和的一半。
带有贪心的证明:如果点
如果钦定补给站为点
树上的带权重心可能有多个,但这些带权重心的答案是一样的,可以参考等号成立的情况来寻找多个带权重心(他们一定时相邻的)。
在原树上,我们可以先钦定一个点作为补给站,通过上述贪心的转移补给站所位于的点即可。
这样的时间复杂度是一次查询
我们需要更高效的算法,其实由于和原树形态有关,很难想到树分治,但总有些人脑回路不正常。
——通过点分树的形式对树进行二分查找。
但由于点分树和原树形态的区别,所以点分树上的一棵子树不可以代表原树中的一棵子树,所以就不可以通过贪心的策略用边转移了。
解决方法是,把一个子树看做一个连通块,将这个连通块的信息(权值和与点数和)集中在子树的根,通过查找的方式,不断跳补给站要求的连通块的子树根,即满足
易证明在原树上满足这样要求的点,在点分树上祖先一定都满足这个关系。
当我们从
简单的,把连通块
上面的算法求出补给站的位置,后面的问题是简单的。
每个点分树上的点存下子树内的权值和与路径乘权值的和,对于此时做单点查询向上收集这些值并去重即可。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=1e5+5;
struct Edge
{
int tot;
int head[maxn];
struct edgenode{int to,nxt,val;}edge[maxn*2];
inline void add(int x,int y,int z)
{
tot++;
edge[tot].to=y;
edge[tot].nxt=head[x];
edge[tot].val=z;
head[x]=tot;
}
}T;
struct ed{int v;ll dis;};
vector<ed>fa[maxn],s[maxn];
int n,m,rt;
ll wtot;
int siz[maxn],itr[maxn];
bool cut[maxn],book[maxn];
ll dep[maxn],rel[maxn],wsiz[maxn];
inline int dfs1(int u)//处理原树 siz
{
book[u]=true;
for(int i=T.head[u];i;i=T.edge[i].nxt)
{
int v=T.edge[i].to;
if(!book[v]&&!cut[v]) siz[u]+=dfs1(v);
}
book[u]=false;return siz[u];
}
inline int fr(int u,const int &tot)//找重心
{
book[u]=true;int ret=u;
for(int i=T.head[u];i;i=T.edge[i].nxt)
{
int v=T.edge[i].to;
if(!book[v]&&!cut[v]&&2*siz[v]>=tot){ret=fr(v,tot);break;}
}
book[u]=false;return ret;
}
inline void dfs2(int u,const int &g)//预处理点分树的距离
{
book[u]=true;fa[u].push_back({g,dep[u]});
for(int i=T.head[u];i;i=T.edge[i].nxt)
{
int v=T.edge[i].to;
if(book[v]||cut[v]) continue;
dep[v]=dep[u]+T.edge[i].val;dfs2(v,g);
}
book[u]=false;siz[u]=1;return ;
}
inline void solve(int u,const int &f)//建点分树
{
dfs1(u);int g=fr(u,siz[u]);cut[g]=true;itr[g]=u;
s[f].push_back({g,0}),fa[g].push_back({g,0});
for(int i=T.head[g];i;i=T.edge[i].nxt)
{
int v=T.edge[i].to;
if(!cut[v]){dep[v]=T.edge[i].val;dfs2(v,g);solve(v,g);}
}
rt=g;
}
inline void modify(int u,int e)//每次修改操作,修改树上点权
{
wtot+=e;int p=u;rel[u]+=e;
for(auto i:fa[u]) wsiz[i.v]+=e;
for(int i=fa[u].size()-1;i>=0;p=fa[u][i].v,i--)
{
for(auto &j:s[fa[u][i].v])if(j.v==p){j.dis+=e*fa[u][i].dis;break;}
}
}
inline void modi(int u,int e){for(auto i:fa[u]) wsiz[i.v]+=e;}//暴力修改点权
inline int find(int u)//树上二分
{
int ret=u;
for(auto i:s[u])
{
if(wsiz[i.v]*2>=wtot)
{
int del=wsiz[u]-wsiz[i.v];
modi(itr[i.v],del);ret=find(i.v);modi(itr[i.v],-del);
break;
}
}
return ret;
}
inline ll qry(int u)//查询
{
ll ret=0;int p=u;
for(int i=fa[u].size()-1;i>=0;p=fa[u][i].v,i--)
{
ret+=fa[u][i].dis*rel[fa[u][i].v];int f=fa[u][i].v;
for(auto j:s[f]) if(j.v!=p){ret+=fa[u][i].dis*wsiz[j.v]+j.dis;}
}
return ret;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) siz[i]=1;
for(int i=1;i<n;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
T.add(x,y,z),T.add(y,x,z);
}
solve(1,0);
for(int i=1,u,e;i<=m;i++)
{
scanf("%d%d",&u,&e);
modify(u,e);
printf("%lld\n",qry(find(rt)));
}
}
Part 4 点分树中的去重问题
- 最值查询:单一的最值查询即使出现重复绝大多数情况也不影响。而针对最大值个数一类问题,可以利用 stl 或者线段树维护来自不同分治中心的最值及其个数。
- 路径和查询:一般在点分树节点的儿子处减去在父亲查询时求和的多余部分。
而使用点分治枚举子节点,在某些情况下可以跳过去从这一步骤。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!