树链剖分是什么,能吃吗?
树剖的思维难度不大,但一般比较难调。
引入
树链剖分就是把一棵树剖成一条条链,视剖的方式分为长链剖分与重链剖分。
所谓重剖,把子树大小最大的那个儿子称为“重儿子”,把树剖为若干条重链,长剖类似,最长的儿子为“长儿子”,剖为若干条长链。由于重剖应用范围比较广(下面会讲),而长剖的用途主要就是维护一些按深度转移、算贡献的 DP,所以平时所说的树剖一般就是重剖。
红边为重边,重剖的效果大致如下:
我们都知道求 LCA 可以用倍增、 Tarjan、ST表求,但其实也可以用树剖求,这里用求 LCA 来引入树剖。
再解释几个定义:
-
轻儿子:不是重儿子就是轻儿子。
-
重边:连接两个重儿子的边。
-
重链:连续的重边连成的一条链。
-
轻边:连接两个轻儿子的边。
-
链头:一条重链深度最浅的点为链头,必定为轻儿子(如果不是就会接上其他重链,显然不对)。
如上图,树剖可以把树分为为从根到叶子的若干条不相交的链,其最关键的性质就是是从任一点出发,到达根节点经过的重链(轻边)不超过
在求
- 两点不在一条重链上,不妨设
的深度较深,一路向上跳,每次跳到链头的父亲。 - 在一条重链上,则深度更浅的点为 LCA。
int lca(int u,int v) {
while(tr[u].top!=tr[v].top) {
if(tr[tr[u].top].dep<tr[tr[v].top].dep) swap(u,v);//不妨设u的深度更深,好让u往上跳
u=tr[tr[u].top].fa;//越过轻边,到下一条重链
}
if(tr[u].dep>tr[v].dep) swap(u,v);
return u;
}
接下来,需要求出以上所需要的数组。
struct trr {
//当前节点为u
//tr[u].(...)
int son;//u的重儿子
int siz;//以u为根子树大小
int dep;//u的深度
int fa;// u的父亲
/*********************/
int id;//u的dfn序,后面要用
int top;//u所在链的链顶
} tr[];
我们需要两个 DFS 来求出以上的数组,上方出示代码的前四个在
int dfs1(int u,int fa) {
tr[u].fa=fa,tr[u].dep=tr[fa].dep+1,tr[u].siz=1;
int mx=-1;
for(int i=head[u]; i; i=edge[i].nxt) {
int v=edge[i].to;
if(v==fa)continue;
tr[u].siz+=dfs1(v,u);
if(tr[v].siz>mx) tr[u].son=v,mx=tr[v].siz;//更新重儿子
}
return tr[u].siz;
}
void dfs2(int u,int tp) {
//tr[u].id=++tot;求dfn序后面用得上
//a[tot]=wi[u];赋点权求LCA暂时也用不上
tr[u].top=tp;//当前链的链顶
if(!tr[u].son) return ;
dfs2(tr[u].son,tp);//优先重儿子
for(int i=head[u]; i; i=edge[i].nxt) {
int v=edge[i].to;
if(tr[v].id)continue;
dfs2(v,v);
}
}
求出这些后,就可用树剖求 LCA 了,可以去 A 掉LCA的板子了。
树链剖分的真正用法
树剖的用途当然不止于求 LCA ,各种进阶用法依赖于一个十分重要的性质:一条重链内部 dfn 序是连续的。
也就是说,求了 dfn 序,才可以完全的“剖”开这棵树,把树上问题转换为线段上的区间问题,线段树正好可以维护。
树剖可以解决不少问题,比较经典的有:
- 修改(查询)
到 的路径上的点权(和) - 修改(查询)以
为根的子树各点点权(和)。
下面以查询
还是这一张图为例,不过先把他的 dfn 序跑出来。
按照上文中
比如说,现在要求 9 到 11 的路径上的权值和,则我们先找出来节点深度较深的那个,然后向上跳到链顶。
(图画的太抽象了)
跳的时候,由于一条重链内的 dfn 序是连续的,用线段树查询一下权值和即可。
然后越过一条轻边(也就是在跳到链头的父亲)。
然后重复以上过程,不断让深度大的那个点向上跳,并用线段树查询,直到两者位于同一条链。
最后一步查询,都已跳到同一链上时,不妨设 tr[u].id
到 tr[v].id
的权值和即可(现在在一条链上,dfn 序自然是连续的)。
修改同理。
查询修改子树时可以更简单一点,由于子树内部的 dfn 序也是连续的,所以我们查询(修改)以 tr[u].id
询问(修改)到 tr[u].id+tr[u]s.iz-1
。
至此,树剖的基本操作完工,可以去过掉板子了
ACcode
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
struct node {
int nxt,to;
} edge[maxn<<1];
int head[maxn<<1],cnt=0,tot=0;
int n,m,rt,mod;
struct trr {
//当前节点为u
//tr[u].(...)
int son;//u的重儿子
int siz;//以u为根子树大小
int dep;//u的深度
int fa;// u的父亲
/************/
int id;//u的dfn序,后面要用
int top;//u所在链的链顶
} tr[maxn];
struct seg {
int l,r,sum,add,siz;
} t[maxn<<2];
int wi[maxn],a[maxn];
void add(int u,int v) {
edge[++cnt].to=v;
edge[cnt].nxt=head[u];
head[u]=cnt;
}
void pushdown(int p) {
if(!t[p].add)return;
t[p*2].sum=(t[p*2].sum+t[p*2].siz*t[p].add)%mod;
t[p*2+1].sum=(t[p*2+1].sum+t[p*2+1].siz*t[p].add)%mod;
t[p*2].add=t[p*2].add+t[p].add;
t[p*2+1].add+=t[p].add;
t[p].add=0;
}
void build(int p,int l,int r) {
t[p].l=l,t[p].r=r,t[p].siz=r-l+1;
if(l==r) {
t[p].sum=a[l]%mod;
return ;
}
int mid=(l+r)/2;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
t[p].sum=(t[p*2].sum+t[p*2+1].sum)%mod;
}
void add(int p,int l,int r,int k) {
if(t[p].l>=l && r>=t[p].r) {
t[p].sum=(t[p].sum+t[p].siz*k)%mod;
t[p].add+=k;
return ;
}
pushdown(p);
int mid=(t[p].l+t[p].r)/2;
if(l<=mid)add(p*2,l,r,k);
if(r>mid) add(p*2+1,l,r,k);
t[p].sum=(t[p*2].sum+t[p*2+1].sum)%mod;
}
int ask(int p,int l,int r) {
if(t[p].l>=l && t[p].r<=r)
return t[p].sum;
pushdown(p);
int ans=0;
int mid=(t[p].r+t[p].l)>>1;
if(l<=mid) ans=(ans+ask(p*2,l,r))%mod;
if(r>mid) ans=(ans+ask(p*2+1,l,r))%mod;
return ans;
}
//**********************************************************************//
int dfs1(int u,int fa) {
tr[u].fa=fa,tr[u].dep=tr[fa].dep+1,tr[u].siz=1;
int mx=-1;
for(int i=head[u]; i; i=edge[i].nxt) {
int v=edge[i].to;
if(v==fa)continue;
tr[u].siz+=dfs1(v,u);
if(tr[v].siz>mx) tr[u].son=v,mx=tr[v].siz;//更新重儿子
}
return tr[u].siz;
}
void dfs2(int u,int tp) {
tr[u].id=++tot;
a[tot]=wi[u];
tr[u].top=tp;//当前链的链顶
if(!tr[u].son) return ;
dfs2(tr[u].son,tp);//优先重儿子
for(int i=head[u]; i; i=edge[i].nxt) {
int v=edge[i].to;
if(tr[v].id)continue;
dfs2(v,v);
}
}
void treeadd(int u,int v,int k) {
while(tr[u].top!=tr[v].top) {
if(tr[tr[u].top].dep<tr[tr[v].top].dep) swap(u,v);
add(1,tr[tr[u].top].id,tr[u].id,k);
u=tr[tr[u].top].fa;
}
if(tr[u].dep>tr[v].dep)
swap(u,v);
add(1,tr[u].id,tr[v].id,k);
}
void treesum(int u,int v) {
int ans=0;
while(tr[u].top!=tr[v].top) {
if(tr[tr[u].top].dep<tr[tr[v].top].dep) swap(u,v);
ans+=ask(1,tr[tr[u].top].id,tr[u].id);
u=tr[tr[u].top].fa;
}
if(tr[u].dep>tr[v].dep)
swap(u,v);
ans=(ans+ask(1,tr[u].id,tr[v].id))%mod;
cout<<ans<<endl;
}
int main() {
cin>>n>>m>>rt>>mod;
for(int i=1; i<=n; i++)
cin>>wi[i];
for(int u,v,i=1; i<=n-1; i++)
cin>>u>>v,add(u,v),add(v,u);
int o=dfs1(rt,0);
dfs2(rt,rt);
build(1,1,n);
while(m--) {
int op,x,y,z;
cin>>op;
if(op==1)cin>>x>>y>>z,treeadd(x,y,z);
if(op==2)cin>>x>>y,treesum(x,y);
if(op==3)cin>>x>>z,add(1,tr[x].id,tr[x].id+tr[x].siz-1,z);
if(op==4) {
cin>>x;
cout<<ask(1,tr[x].id,tr[x].id+tr[x].siz-1)<<endl;
}
}
return 0;
}
其他小技巧
很多时候题目给我们的是边权而非点权,那该怎办。
这里有一个小技巧,我们都知道每个节点至多只可能有一个父亲,所以我们将一个点与他父亲的边之边权转为他的点权即可。
图片待施工。
具体到题目中,一般会给出边的编号进行查询、修改,我们只要判断一下两点的深度深浅即可判断该用谁的点权。
但在查询修改时,我们发现,对于某两个点的 LCA 的点权根本不是原先路径上的边权,所以我们要扣去这个点,其实好办,在查询“跳链”最后两者都在一条链上时,我们设点 tr[u].id+1
。
P1505 [国家集训队] 旅游就是一道不错的边权转点权的题目,区间取反要考虑好标记之间的转换。
结语
学了树剖之后就可以水一大堆蓝紫题了
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】