树链剖分入门
本人初学,若有错误恳请大佬在评论区指出,谢谢!
一,它能干嘛
恶心你
解决树上路径/子树等问题。
情景引入
老师:给一棵点权树和一些操作,每次操作选两个点,把这两个点之间的路径上的点权加上一个数
同学:这不就是树上差分+LCA吗
老师:每次操作过后可能会问你两点之间的路径上的点权之和
同学:????
老师:再加上子树操作
同学:群青.jpg
不扯了
比如Luogu的模板:P3384
已知一棵包含 \(N\) 个结点的树(连通且无环),每个节点上包含一个数值。
\(M\) 个操作:
1 x y z
,表示将树从 \(x\) 到 \(y\) 结点最短路径上所有节点的值都加上 \(z\)。2 x y
,表示求树从 \(x\) 到 \(y\) 结点最短路径上所有节点的值之和。3 x z
,表示将以 \(x\) 为根节点的子树内所有节点值都加上 \(z\)。4 x
表示求以 \(x\) 为根节点的子树内所有节点值之和\(N,M\le10^5\)
树链剖分可以在 \(O(n\log ^2 n)\) 的时间内解决这个问题。
二,怎么做
顾名思义,树链剖分就是要把树解剖成一条一条的链,然后把树上问题转换为区间问题,使用线段树等数据结构求解。树那么可爱你为什么要把它解剖了你食不食人啊
OI中一般采取轻重链剖分。
几个概念:
轻、重儿子:一个结点中的所有儿子中,子树大小最大的儿子为重儿子,其余为轻儿子。
轻、重边:连接父结点到重儿子的边叫做重边,连接父结点到轻儿子的边叫做轻边。
重链:由多条重边连接而成的路径。特别的,不在其他重链上的单个节点也算一条重链。
比如下面这棵树,重儿子用红色标出,重边用蓝色标出,方框内为重链:
写程序的准备
你要开这些东西:
int dep[M],dfn[M],rnk[M],fa[M],DFN,top[M],siz[M],hson[M];
dep
:结点的深度。
dfn
:结点的dfs序。( 不是 欧拉序)
rnk
:记录dfs序的第几位是几号结点。
fa
:结点的父亲。
top
:这个结点所在重链的链顶(深度最小的点)。
siz
:子树大小
hson
:重儿子的编号。
预处理
使用树剖要首先做两次遍历。
第一次:处理出每个结点的父亲、重儿子、子树大小、深度。
void dfs1(int x,int f){
dep[x]=dep[f]+1;siz[x]=1;hson[x]=0;fa[x]=f;
for(int i=h[x],v;v=e[i].v,i;i=e[i].nxt)if(v!=f){
dfs1(v,x);siz[x]+=siz[v];
if(siz[hson[x]]<siz[v])hson[x]=v;
}
}
第二次:处理出每个结点的dfs序、rnk
、链顶。
注意:这次dfs先走重儿子,待会说原因。
void dfs2(int x,int tp){
dfn[x]=++DFN;rnk[DFN]=x;top[x]=tp;
if(hson[x])dfs2(hson[x],tp);
for(int i=h[x],v;v=e[i].v,i;i=e[i].nxt)if(v!=fa[x]&&v!=hson[x])dfs2(v,v);
}
这张图中结点上的小数字是dfn:
接下来开一棵区间加区间求和的线段树并 build
。
注意这棵线段树维护的是在dfn中的区间和,故 build
时赋值为 a[rnk[]]
。
void build(int p=1,int s=1,int t=n){
if(s==t){
T[p]=a[rnk[s]];
return;
}
build(LS,s,MID);build(RS,MID+1,t);
pushup(p);
}
这两次dfs和线段树的 build
就是树剖的预处理。
那么如何询问呢?
子树加、子树求和
这个比较简单,根据dfs序的性质,一棵子树在dfs序中肯定是连续的一段。
因此直接找到子树根的 dfn
,要查/改的区间就是 dfn[p],dfn[p]+siz[p]-1
。
void modifysub(int p,int x){
SGT::modify(dfn[p],dfn[p]+siz[p]-1,x);
}
int querysub(int p){
return SGT::query(dfn[p],dfn[p]+siz[p]-1);
}
链加、链求和
前方高能
一条链在dfs序中好像没有什么规律可循...
...真的吗?
显然,所有的结点都有唯一一条重链与之对应。高中数学乱入
那么这意味着一条路径可以被拆成多条路径,每一条路径都只被一条重链包含。
考虑为什么我们要在标dfn的时候先走重儿子。
再仔细看看这个图,有没有什么头猪?
没错,这样处理之后每条重链在dfs序上就是连续的一段了,从top向下递增。
那么我们就可以设计以下算法:
从两个查询的结点一直向上跳,每一次跳跃选取链顶深度较大的一个查询/修改对应重链链顶到这个点的区间和/执行区间加操作并跳到这条链链顶的父亲,直到跳到同一条重链上,再加上/修改剩下的一部分。
好绕啊
给个跳跃的图解(以求和为例):
假设我们要查询结点8到结点6的和。
第一步:结点8的链顶比结点6的链顶更深,于是我们查询8的链顶(8)到8这一段dfs序内的和([5,5])并跳到4号结点。(用绿色圈出的点为已经加到答案的部分)
第二步:结点6的链顶比结点4的链顶更深,于是我们查询6的链顶(3)到6这一段dfs序内的和([7,8])并跳到1号结点。
第三步:结点4与结点1在同一条重链上,于是我们查询1到4这一段dfs序的和([1,4]),结束。
因此,我们成功求出了两点之和。
顺便发现了新的求LCA算法
void modifych(int u,int v,int x){
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]])std::swap(u,v);
SGT::modify(dfn[top[u]],dfn[u],x);u=fa[top[u]];
}
if(dep[u]>dep[v])std::swap(u,v);
SGT::modify(dfn[u],dfn[v],x);
}
int querych(int u,int v){
int ans=0;
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]])std::swap(u,v);
ans=(ans+SGT::query(dfn[top[u]],dfn[u])%P)%P;u=fa[top[u]];
}
if(dep[u]>dep[v])std::swap(u,v);
ans=(ans+SGT::query(dfn[u],dfn[v])%P)%P;
return ans;
}
复杂度证明
显然,如果 \(u\) 是 \(v\) 的父亲且 \((u,v)\) 是一条轻边,那么 \(siz_u \ge 2siz_v\)(否则 \(u\) 的重儿子就应当是 \(v\))
那么一条路径从LCA处拆开,两条路径上最多有 \(\log n\) 条重链。
乘上线段树的复杂度,因此链相关的操作复杂度为 \(\log^2 n\),子树相关操作复杂度为 \(\log n\)。
模板完整代码
#include<cstdio>
#define int long long
const int M=1e5+5;
inline int read(){int x(0),op(0);char ch=getchar();while(ch<'0'||ch>'9')op|=(ch==45),ch=getchar();while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+(ch^48),ch=getchar();return op?-x:x;}
int n,m,r,P;
int a[M];
struct edge{
int v,nxt;
}e[M<<1];
int h[M],etot;
void adde(int u,int v){e[++etot]=(edge){v,h[u]};h[u]=etot;}
namespace HLD{
int fa[M],siz[M],hson[M],dfn[M],top[M],rnk[M],dep[M];
int DFN;
void dfs1(int x,int f){
dep[x]=dep[f]+1;siz[x]=1;hson[x]=0;fa[x]=f;
for(int i=h[x],v;v=e[i].v,i;i=e[i].nxt)if(v!=f){
dfs1(v,x);siz[x]+=siz[v];
if(siz[hson[x]]<siz[v])hson[x]=v;
}
}
void dfs2(int x,int tp){
dfn[x]=++DFN;rnk[DFN]=x;top[x]=tp;
if(hson[x])dfs2(hson[x],tp);
for(int i=h[x],v;v=e[i].v,i;i=e[i].nxt)if(v!=fa[x]&&v!=hson[x])dfs2(v,v);
}
namespace SGT{
#define LS (p<<1)
#define RS ((p<<1)|1)
#define MID ((s+t)>>1)
int T[M<<2],tag[M<<2];
void pushup(int p){T[p]=(T[LS]+T[RS])%P;}
void pushdown(int p,int s,int t){
if(tag[p]){
T[LS]=(T[LS]+(tag[p]*(MID-s+1))%P)%P;T[RS]=(T[RS]+(tag[p]*(t-MID))%P)%P;
tag[LS]=(tag[LS]+tag[p])%P;tag[RS]=(tag[RS]+tag[p])%P;
}
tag[p]=0;
}
void build(int p=1,int s=1,int t=n){
if(s==t){
T[p]=a[rnk[s]];
return;
}
build(LS,s,MID);build(RS,MID+1,t);
pushup(p);
}
int query(int l,int r,int p=1,int s=1,int t=n){
if(t<l||r<s)return 0;
if(l<=s&&t<=r)return T[p];
pushdown(p,s,t);
return (query(l,r,LS,s,MID)+query(l,r,RS,MID+1,t))%P;
}
void modify(int l,int r,int x,int p=1,int s=1,int t=n){
if(t<l||r<s)return;
if(l<=s&&t<=r){
T[p]=(T[p]+(t-s+1)*x%P)%P;
tag[p]=(tag[p]+x)%P;
return;
}
pushdown(p,s,t);
modify(l,r,x,LS,s,MID);modify(l,r,x,RS,MID+1,t);
pushup(p);
}
}
void init(){dfs1(r,0);dfs2(r,r);SGT::build();}
void modifych(int u,int v,int x){
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]])std::swap(u,v);
SGT::modify(dfn[top[u]],dfn[u],x);u=fa[top[u]];
}
if(dep[u]>dep[v])std::swap(u,v);
SGT::modify(dfn[u],dfn[v],x);
}
void modifysub(int p,int x){
SGT::modify(dfn[p],dfn[p]+siz[p]-1,x);
}
int querych(int u,int v){
int ans=0;
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]])std::swap(u,v);
ans=(ans+SGT::query(dfn[top[u]],dfn[u])%P)%P;u=fa[top[u]];
}
if(dep[u]>dep[v])std::swap(u,v);
ans=(ans+SGT::query(dfn[u],dfn[v])%P)%P;
return ans;
}
int querysub(int p){
return SGT::query(dfn[p],dfn[p]+siz[p]-1);
}
}
signed main(){
n=read(),m=read(),r=read(),P=read();
for(int i=1;i<=n;++i)a[i]=read();
for(int i=1;i<n;++i){
int u=read(),v=read();
adde(u,v);adde(v,u);
}
HLD::init();
while(m--){
int op=read(),u=read();
if(op<4){
int v=read();
if(op==1){
int x=read();
HLD::modifych(u,v,x);
}
else if(op==2){
printf("%lld\n",HLD::querych(u,v));
}
else HLD::modifysub(u,v);
}
else printf("%lld\n",HLD::querysub(u));
}
return 0;
}
例题
建议改为:树链剖分模板2。
代码:咕了
建议改为:树链剖分模板3。
代码:(std::vector
ver.)
#include<cstdio>
#include<vector>
#define int long long
const int M=1e5+5;
inline int read(){int x(0),op(0);char ch=getchar();while(ch<'0'||ch>'9')op|=(ch==45),ch=getchar();while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+(ch^48),ch=getchar();return op?-x:x;}
int n,m,r,P;
int a[M];
namespace HLD{
std::vector<int> T[M];
int fa[M],siz[M],hson[M],dfn[M],top[M],rnk[M],dep[M];
int DFN;
namespace SGT{
#define LS (p<<1)
#define RS ((p<<1)|1)
#define MID ((s+t)>>1)
int SGT[M<<2],tag[M<<2];
void pushup(int p){SGT[p]=SGT[LS]+SGT[RS];}
void pushdown(int p,int s,int t){
if(tag[p]){
SGT[LS]+=tag[p]*(MID-s+1);SGT[RS]+=tag[p]*(t-MID);
tag[LS]+=tag[p];tag[RS]+=tag[p];
}
tag[p]=0;
}
void build(int p=1,int s=1,int t=n){
if(s==t){
SGT[p]=a[rnk[s]];
return;
}
build(LS,s,MID);build(RS,MID+1,t);
pushup(p);
}
int query(int l,int r,int p=1,int s=1,int t=n){
if(t<l||r<s)return 0;
if(l<=s&&t<=r)return SGT[p];
pushdown(p,s,t);
return query(l,r,LS,s,MID)+query(l,r,RS,MID+1,t);
}
void modify(int l,int r,int x,int p=1,int s=1,int t=n){
if(t<l||r<s)return;
if(l<=s&&t<=r){
SGT[p]+=(t-s+1)*x;tag[p]+=x;
return;
}
pushdown(p,s,t);
modify(l,r,x,LS,s,MID);modify(l,r,x,RS,MID+1,t);
pushup(p);
}
}
void dfs1(int x,int f){
fa[x]=f;siz[x]=1;hson[x]=0;dep[x]=dep[f]+1;
for(auto v:T[x])if(v!=f){
dfs1(v,x);
siz[x]+=siz[v];
if(siz[v]>siz[hson[x]])hson[x]=v;
}
}
void dfs2(int x,int f){
dfn[x]=++DFN;rnk[dfn[x]]=x;top[x]=f;
if(!hson[x])return;
dfs2(hson[x],f);
for(auto v:T[x])if(v!=fa[x]&&v!=hson[x])dfs2(v,v);
}
void init(){
dfs1(r,0);
dfs2(r,r);
SGT::build();
}
void modifypt(int p,int x){
SGT::modify(dfn[p],dfn[p],x);
}
void modifysub(int p,int x){
SGT::modify(dfn[p],dfn[p]+siz[p]-1,x);
}
int querych(int u,int v){
int ans=0;
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]])std::swap(u,v);
ans+=SGT::query(dfn[top[u]],dfn[u]);u=fa[top[u]];
}
if(dep[u]>dep[v])std::swap(u,v);
ans+=SGT::query(dfn[u],dfn[v]);
return ans;
}
}
signed main(){
n=read(),m=read(),r=1;
for(int i=1;i<=n;++i)a[i]=read();
for(int i=1;i<n;++i){
int u=read(),v=read();
HLD::T[u].push_back(v);HLD::T[v].push_back(u);
}
HLD::init();
while(m--){
int op=read(),u=read();
if(op<3){
int k=read();
if(op==1)HLD::modifypt(u,k);
else HLD::modifysub(u,k);
}
else printf("%lld\n",HLD::querych(1,u));
}
return 0;
}
建议改为:树链剖分模板4
好吧还是要想想的。
这道题可以转换为:
一棵树,初始所有点点权为0。
两种操作:
-
选一个结点,把它到根的路径上所有点点权改为1,并询问改之前和改之后路径上点权和的差。
-
选一个结点,把它的子树中所有点点权改为0,并询问改之前和改之后子树中点权和的差。
于是成裸题了。
代码:咕了
难点在线段树上。
线段树记左右端点的颜色以及求的答案,query合并答案的时候如果左区间的最右颜色和右区间最左颜色相同答案减1。
加个懒标记即可。
代码:咕了
咕了的代码有(zai)空(ye)再(bu)写(xie)吧(le)
三,换根树剖
和Luogu模板不同的是,这道题有一个换根操作。
显然你不可能换一次根就重新预处理一遍除非你能 \(n^2\) 过十万
但是换根是不影响链操作的,所以讨论子树操作怎么搞。
显然,有3种情况:
-
现在的根不在操作结点的子树内:说明这个结点的子树没变,这种情况下直接像原来那样就行了。
-
现在的根就是要操作的结点:直接全局操作就好了。
-
现在的根在操作结点的子树内:相当于要操作含根子树以外的所有结点,因此我们要找出哪个子树包含当前的根。
我们从现在的根沿着重链一步一步的向上跳,直到跳到当前操作结点的重链上或者该重链链顶的父亲就是操作结点。
第一种情况下,操作结点的重儿子即为所求;第二种情况下,该重链的链顶即为所求。
int getson(int x,int y){
for(;top[x]!=top[y];x=fa[top[x]])if(fa[top[x]]==y)return top[x];
return hson[y];
}
啊终于写完了累死我了
有空更新。
标签:结点,入门,剖分,int,siz,top,树链,dfn,重链 From: https://www.cnblogs.com/pokefunc/p/16873125.html