浅谈轻重链剖分
前置知识
dfs、线段树。
引入
考虑两个问题:
-
将 x 到 y 的路径上的点的权值加上 z。
对于这个问题,可以用树上差分愉快的解决。 -
求出 x 到 y 的路径上的点的权值和。
dfs 加 LCA 就可以解决。
现在,我们将两个问题综合起来,那么很明显的,我们可以每次查询时都 dfs 一次,时间复杂度 \(\mathcal O(nm)\)。
这显然不是很优秀,那有没有比较好的做法呢?
这个时候,树链剖分就可以派上用场了。
介绍
树链剖分,即将树剖分成若干条链进行处理的方法。本质上,是一些数据结构或者算法在树上的推广。
例题
题目描述
如题,已知一棵包含 \(N\) 个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:
-
1 x y z
,表示将树从 \(x\) 到 \(y\) 结点最短路径上所有节点的值都加上 \(z\)。 -
2 x y
,表示求树从 \(x\) 到 \(y\) 结点最短路径上所有节点的值之和。 -
3 x z
,表示将以 \(x\) 为根节点的子树内所有节点值都加上 \(z\)。 -
4 x
表示求以 \(x\) 为根节点的子树内所有节点值之和
\(1\le N\le 10^5\),\(1\le M\le 10^5\),\(1\le R\le N\),\(1\le P \le 2^{31}-1\)。
例题分析
首先先不管树,看到一段范围内的加和求和,应该会有线段树等数据结构的想法,那么如何将序列上的操作扩展到树呢?
如果我们的树是由多段序列组成的,那么我们就可以通过多次线段树的操作来完成整个的更新或查询操作。而树上的序列,最直接的就是链。所以我们要将树给剖成多条链。
剖成链之后,再将线段树通过若干条链扩展到树上即可。
算法分步讲解
而为了使算法更加优秀,我们采取特殊的剖法。
定义重儿子为儿子中子树最大的儿子,其他儿子为轻儿子。重边为当前点连向重儿子的那条边,其余边为轻边。由重边构成的链叫做重链,而由轻边构成的链叫做轻链。
那么这样的话,整棵树就被剖成了若干重链或轻链。
dfs1——确定重儿子
重儿子既然是儿子中子树最大的,那么就可以通过 dfs 求出每个节点子树的大小,然后取个最大值即可。
另外,在这个过程中,顺便将每个点的父亲和深度求出来。
void dfs1(int x,int fa,int deep)
{
dep[x]=deep;f[x]=fa;size[x]=1;
for (int i=a[x].head;i;i=a[i].next)
{
int v=a[i].to;
if (v==fa) continue;
dfs1(v,x,deep+1);
size[x]+=size[v];
if (size[v]>=size[son[x]]) son[x]=v;
}
}
dfs2——确定每个点的编号
我们知道,线段树每次更新(或查询)都是对于区间的操作,所以我们要让每次在树上的更新也是对于连续的编号。
而题目要求更新(或查询)的分别是重链和整个子树,所以说 dfs2 所确定的编号要保证每条重链和每个子树的编号都是连续的。
因此,在 dfs2 的过程中,我们优先选择重儿子进行 dfs2,这样就可以保证重链的编号是连续的。然后又由于 dfs 的特性,子树一定是连续编号。这样,两个条件都达到了。
在这次 dfs 中,我们也可以将每个点所在的重链的链头找到(若所在的链为轻链,则链头标记为自身),记为 \(top_x\)。
void dfs2(int x,int topf)
{
dfn[x]=++cnt;
v1[cnt]=val[x];//记录在新的编号下每个点的取值
top[x]=topf;
if (son[x]) dfs2(son[x],topf);
for (int i=a[x].head;i;i=a[i].next)
{
int v=a[i].to;
if (v==f[x]||v==son[x]) continue;
dfs2(v,v);
}
}
至此,通过两遍 dfs,我们已经将树剖成了若干条链并且记录了相关数据,接下来考虑怎么实现我们的操作。
对子树的更新或查询
由于一整棵子树的编号是连续的,于是可以直接在线段树上更新或查询,只需要注意编号的问题。
void son_modify(int x,int y) {modify(1,1,n,dfn[x],dfn[x]+size[x]-1,y);}
int son_query(int x)
{
s=0;
query(1,1,n,dfn[x],dfn[x]+size[x]-1);
return s;
}
对路径的更新和查询
因为树已经被剖分好了,所以树上的任意一条路径肯定可以由若干条重链或轻点连接起来。
并且可以证明,任意一个点到根的路径上不会超过 \(\log n\) 条重链。
这样的话,只需要对于每个重链进行一次更新或查询,然后跃至链头的父亲,就可以完成对路径的更新或查询。
同时,由于两个点可能不在同一条重链上,因此每次要跳的是深度更深的那个点,直到这两个点在同一条重链上。
void range_modify(int x,int y,int z)
{
z%=mod;
while (top[x]!=top[y])
{
if (dep[top[x]]<dep[top[y]]) swap(x,y);
modify(1,1,n,dfn[top[x]],dfn[x],z);
x=f[top[x]];
}
if (dep[x]>dep[y]) swap(x,y);
modify(1,1,n,dfn[x],dfn[y],z);
}
int range_query(int x,int y)
{
int ans=0;
while (top[x]!=top[y])
{
if (dep[top[x]]<dep[top[y]]) swap(x,y);
s=0;
query(1,1,n,dfn[top[x]],dfn[x]);
ans=(ans+s)%mod;
x=f[top[x]];
}
if (dep[x]>dep[y]) swap(x,y);
s=0;
query(1,1,n,dfn[x],dfn[y]);
ans=(ans+s)%mod;
return ans;
}
线段树的部分就不用多说了,就是普通的区间修改、区间查询。
复杂度分析
树链剖分有两个性质:
-
如果 \((x,y)\) 是一条轻边,那么 \(size_y<\frac{size_x}{2}\)。
-
从根节点到任意节点的路径所经过的轻重链个数都必定小于 \(\log n\)。
因此,可以证明总复杂度是 \(\mathcal O(nlog^2n)\)
完整代码
#include<cstdio>
#include<algorithm>
#define N 200005
using namespace std;
struct node
{
int to,next,head;
}a[N<<1];
struct s_tree
{
int val,lazy;
}tree[N<<2];
int n,m,root,mod,x,y,z,s,tot,cnt,opt,val[N],v1[N],f[N],dep[N],top[N],size[N],dfn[N],son[N];
void add(int x,int y)
{
a[++tot].to=y;
a[tot].next=a[x].head;
a[x].head=tot;
}
void dfs1(int x,int fa,int deep)
{
dep[x]=deep;
f[x]=fa;
size[x]=1;
int maxson=-1;
for (int i=a[x].head;i;i=a[i].next)
{
int v=a[i].to;
if (v==fa) continue;
dfs1(v,x,deep+1);
size[x]+=size[v];
if (size[v]>=maxson) son[x]=v,maxson=size[v];
}
}
void dfs2(int x,int topf)
{
dfn[x]=++cnt;
v1[cnt]=val[x];
top[x]=topf;
if (!son[x]) return;
dfs2(son[x],topf);
for (int i=a[x].head;i;i=a[i].next)
{
int v=a[i].to;
if (v==f[x]||v==son[x]) continue;
dfs2(v,v);
}
}
void pushdown(int x,int len)
{
tree[x<<1].lazy+=tree[x].lazy;
tree[x<<1|1].lazy+=tree[x].lazy;
tree[x<<1].val=(tree[x<<1].val+tree[x].lazy*(len-(len>>1)))%mod;
tree[x<<1|1].val=(tree[x<<1|1].val+tree[x].lazy*(len>>1))%mod;
tree[x].lazy=0;
}
void build(int x,int l,int r)
{
if (l==r)
{
tree[x].val=v1[l]%mod;
return;
}
int mid=(l+r)>>1;
build(x<<1,l,mid);
build(x<<1|1,mid+1,r);
tree[x].val=(tree[x<<1].val+tree[x<<1|1].val)%mod;
}
void modify(int x,int l,int r,int p,int q,int v)
{
if (p<=l&&q>=r)
{
tree[x].lazy+=v;
tree[x].val+=v*(r-l+1);
return;
}
int mid=(l+r)>>1;
if (tree[x].lazy) pushdown(x,r-l+1);
if (p<=mid) modify(x<<1,l,mid,p,q,v);
if (q>mid) modify(x<<1|1,mid+1,r,p,q,v);
tree[x].val=(tree[x<<1].val+tree[x<<1|1].val)%mod;
}
void query(int x,int l,int r,int p,int q)
{
if (p<=l&&q>=r)
{
s=(s+tree[x].val)%mod;
return;
}
int mid=(l+r)>>1;
if (tree[x].lazy) pushdown(x,r-l+1);
if (p<=mid) query(x<<1,l,mid,p,q);
if (q>mid) query(x<<1|1,mid+1,r,p,q);
}
void range_modify(int x,int y,int z)
{
z%=mod;
while (top[x]!=top[y])
{
if (dep[top[x]]<dep[top[y]]) swap(x,y);
modify(1,1,n,dfn[top[x]],dfn[x],z);
x=f[top[x]];
}
if (dep[x]>dep[y]) swap(x,y);
modify(1,1,n,dfn[x],dfn[y],z);
}
int range_query(int x,int y)
{
int ans=0;
while (top[x]!=top[y])
{
if (dep[top[x]]<dep[top[y]]) swap(x,y);
s=0;
query(1,1,n,dfn[top[x]],dfn[x]);
ans=(ans+s)%mod;
x=f[top[x]];
}
if (dep[x]>dep[y]) swap(x,y);
s=0;
query(1,1,n,dfn[x],dfn[y]);
ans=(ans+s)%mod;
return ans;
}
void son_modify(int x,int y) {modify(1,1,n,dfn[x],dfn[x]+size[x]-1,y);}
int son_query(int x)
{
s=0;
query(1,1,n,dfn[x],dfn[x]+size[x]-1);
return s;
}
int main()
{
scanf("%d%d%d%d",&n,&m,&root,&mod);
for (int i=1;i<=n;++i)
scanf("%d",&val[i]);
for (int i=1;i<n;++i)
{
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
dfs1(root,0,1);
dfs2(root,root);
build(1,1,n);
while (m--)
{
scanf("%d",&opt);
if (opt==1)
{
scanf("%d%d%d",&x,&y,&z);
range_modify(x,y,z);
}
if (opt==2)
{
scanf("%d%d",&x,&y);
printf("%d\n",range_query(x,y));
}
if (opt==3)
{
scanf("%d%d",&x,&y);
son_modify(x,y);
}
if (opt==4)
{
scanf("%d",&x);
printf("%d\n",son_query(x));
}
}
return 0;
}
写在最后
树链剖分本质上是将树剖分成链进而使得一些数据结构和算法可以扩展到树上,而剖成链的方法有很多种,本文写的是轻重链剖分,此外还有长链剖分等,有兴趣的可以自行了解。