树链剖分
树链剖分是一种将树转化为一条链的算法,通常和线段树,树状数组,DP等针对链的算法结合使用。
正如题目所说,本文只讲轻重链剖分
轻重链剖分
概念定义
-
重儿子/轻儿子
求出每棵子树的大小,节点数最多的一个子树的根节点就是这个节点父节点的重儿子。
轻儿子即是节点个数最少的子树的根节点。
-
重边/轻边
重边即是连接重儿子与其父亲的边。
轻边即是连接轻儿子与其父亲的边。
-
重链/轻链
重链是由重边组成的极大路径
轻链是由轻边组成的极大路径
单独一个点也可以作为一个特殊的重链/轻链存在,所以对于任意一个节点,都一定在一个重链里面
做法
预处理
首先扔出一个定理:
树中任意一条路径均可以拆分成一条链上 (O(log n)) 个连续区间。
证明+构造:一个DFS序就行了
但是实际代码实现中,一般都是优先遍历重儿子,这样遍历过后,可以保证重链的编 号是连续的。
这个性质尤其有用。
有了这种DFS序,我们可以让上面的那个定理更确切一点:
树中的每一条路径,都可以拆分为 (O(log n )) 条重链(重链可能不完整),即可拆分为 (O(log n)) 个连续区间
所以首先我们要做的是两个DFS对树进行预处理
第一次DFS:找到重儿子
第二次DFS:建立DFS序,根据第一次找到的重儿子找重链
void dfs1(int x,int f) //寻找重儿子
{
dpt[x]=dpt[f]+1; //记录深度
fa[x]=f; //记录父节点
size_[x]=1; //子树至少有他自己
for(int i=head[x];i;i=nxt[i])
{
int y=ver[i];
if(y==f) continue;
dfs1(y,x);
size_[x]+=size_[y]; //子树大小加起来
if(size_[son[x]]<size_[y]) son[x]=y; //打擂台记录重儿子
}
}
void dfs2(int x,int t) //t:当前重链的起点是谁
{
id[x]=++cnt; //记录DFS序
nwpoi[cnt]=poi[x]; //节点的权值重新排序
top[x]=t; //记录这个节点所在重链的起点
if(!son[x]) return ;
dfs2(son[x],t); //优先搜索重儿子,重儿子一定在当前这条重链内
for(int i=head[x];i;i=nxt[i])
{
int y=ver[i];
if(y==fa[x]||y==son[x]) continue;
dfs2(y,y); //轻儿子一定是另外一条重链的开头
}
}
处理完过后, (nwpoi) 数组就是DFS序下的点权数组, (id) 数组就是每个点在DFS下对应的编号, (top) 就是每个节点所在重链的起始节点
查询与操作:
那么现在来以对于树的四种个基本操作为例子,试着初步运用我们所得到的信息
-
1:求以 (x) 为根节点的子树内所有节点值之和
-
2:将以 (x) 为根节点的子树内所有节点值都加上 (z)。
-
3: 将树从 (x) 到 (y) 结点最短路径上所有节点的值都加上 (z)
-
4: 求树从 (x) 到 (y) 结点最短路径上所有节点的值之和。
其实就是P3384树链剖分
这四种操作关键在于两个点:如何找到覆盖一个节点的子树的所有区间 与 如何找到覆盖一条路径的所有区间。找到之后就可以用线段树或树状数组来操作,程序中使用的是线段树。
-
子树查找
一个节点的子树很简单,它在DFS序中是连续的一段,长度就是其子树的大小。
完成这个过程的函数如下:
void update_tree(int x,int k)
{
int l=id[x],r=id[x]+size_[x]-1; //子树的dfs序是连续的一段
update(1,1,n,l,r,k);
}
ll query_tree(int x)
{
int l=id[x],r=id[x]+size_[x]-1;
return query(1,1,n,l,r);
}
-
路径查找
那路径呢?
考虑一下LCA。
遵循从特殊到一般的原则,先想想路径两端点若是在同一个重链中会怎样。
由于我们在搜索时令重链编号连续,所以它们是一个连续区间,直接线段树就好了。
如果在不同的重链中呢?
我们就要去看深度最深的那个节点的重链起点,显然它的重链起点和另外一个节点一开始也不应该是相等的。于是我们继续看它的重链起点的重链起点......直到它们重链起点相同,也就是在同一重链时为止。
这是一个类似于倍增LCA往上跳的过程,但这里每次跳跃到的是当前重链的起点。每跳一次做一次线段树。
完成这个过程的函数如下:
void update_path(int x,int y,int k)
{
while(top[x]!=top[y]) //如果不在同一重链
{
if(dpt[top[x]]<dpt[top[y]]) swap(x,y);
update(1,1,n,id[top[x]],id[x],k);
x=fa[top[x]]; //向上跳,直到位于同一重链
}
if(dpt[x]<dpt[y]) swap(x,y);
update(1,1,n,id[y],id[x],k);
}
ll query_path(int x,int y)
{
ll res=0;
while(top[x]!=top[y])
{
if(dpt[top[x]]<dpt[top[y]]) swap(x,y);
res+=query(1,1,n,id[top[x]],id[x]);
x=fa[top[x]];
}
if(dpt[x]<dpt[y]) swap(x,y);
res+=query(1,1,n,id[y],id[x]);
return res;
}
以上就是轻重链剖分的基本操作了。
时间复杂度
预处理 (O(n))
处理一条路径 (O(log^2n))
处理一棵子树 (O(log n))
完整代码
Code:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e6+10,M=N<<1;
int head[N],ver[M],nxt[M],tot=0;
void add(int x,int y)
{
ver[++tot]=y;
nxt[tot]=head[x];
head[x]=tot;
}
int n,m;
int poi[N];
int id[N],nwpoi[N],cnt=0;
int dpt[N],size_[N],top[N]; //每个点的深度,每个点为根的子树大小,所在重链的顶点
int fa[N],son[N];//父节点,重儿子
ll tree[N<<2],lazy[N<<2];//线段树相关
void dfs1(int x,int f) //寻找重儿子
{
dpt[x]=dpt[f]+1; //记录深度
fa[x]=f; //记录父节点
size_[x]=1; //子树至少有他自己
for(int i=head[x];i;i=nxt[i])
{
int y=ver[i];
if(y==f) continue;
dfs1(y,x);
size_[x]+=size_[y]; //子树大小加起来
if(size_[son[x]]<size_[y]) son[x]=y; //打擂台记录重儿子
}
}
void dfs2(int x,int t) //t:当前重链的起点是谁
{
id[x]=++cnt; //记录DFS序
nwpoi[cnt]=poi[x]; //节点的权值重新排序
top[x]=t; //记录这个节点所在重链的起点
if(!son[x]) return ;
dfs2(son[x],t); //优先搜索重儿子,重儿子一定在当前这条重链内
for(int i=head[x];i;i=nxt[i])
{
int y=ver[i];
if(y==fa[x]||y==son[x]) continue;
dfs2(y,y); //轻儿子一定是另外一条重链的开头
}
}
inline void push_up(int node)
{
tree[node]=tree[node<<1]+tree[node<<1|1];
}
void func(int node,int start,int end,int k)
{
lazy[node]=lazy[node]+k;
tree[node]=tree[node]+k*(end-start+1);
}
void push_down(int node,int start,int end)
{
if(lazy[node]==0) return;
ll mid=start+end>>1;
int lnode=node<<1;
int rnode=node<<1|1;
func(lnode,start,mid,lazy[node]);
func(rnode,mid+1,end,lazy[node]);
lazy[node]=0;
}
void build(int node,int start,int end)
{
lazy[node]=0;
if(start==end)
{
tree[node]=nwpoi[start];
return ;
}
int mid=start+end>>1;
int lnode=node<<1;
int rnode=node<<1|1;
build(lnode,start,mid);
build(rnode,mid+1,end);
push_up(node);
}
void update(int node,int start,int end,int l,int r,int val)
{
if(l<=start&&end<=r)
{
tree[node]+=val*(end-start+1);
lazy[node]+=val;
return ;
}
push_down(node,start,end);
int mid=start+end>>1;
int lnode=node<<1;
int rnode=node<<1|1;
if(l<=mid) update(lnode,start,mid,l,r,val);
if(r>mid) update(rnode,mid+1,end,l,r,val);
push_up(node);
}
ll query(int node,int start,int end,int l,int r)
{
if(end<l||start>r) return 0;
if(l<=start&&end<=r)
return tree[node];
push_down(node,start,end);
int mid=start+end>>1;
int lnode=node<<1;
int rnode=node<<1|1;
ll lsum=query(lnode,start,mid,l,r);
ll rsum=query(rnode,mid+1,end,l,r);
return lsum+rsum;
push_up(node);
}
void update_path(int x,int y,int k)
{
while(top[x]!=top[y]) //如果不在同一重链
{
if(dpt[top[x]]<dpt[top[y]]) swap(x,y);
update(1,1,n,id[top[x]],id[x],k);
x=fa[top[x]]; //向上跳,直到位于同一重链
}
if(dpt[x]<dpt[y]) swap(x,y);
update(1,1,n,id[y],id[x],k);
}
ll query_path(int x,int y)
{
ll res=0;
while(top[x]!=top[y])
{
if(dpt[top[x]]<dpt[top[y]]) swap(x,y);
res+=query(1,1,n,id[top[x]],id[x]);
x=fa[top[x]];
}
if(dpt[x]<dpt[y]) swap(x,y);
res+=query(1,1,n,id[y],id[x]);
return res;
}
void update_tree(int x,int k)
{
int l=id[x],r=id[x]+size_[x]-1; //子树的dfs序是连续的一段
update(1,1,n,l,r,k);
}
ll query_tree(int x)
{
int l=id[x],r=id[x]+size_[x]-1;
return query(1,1,n,l,r);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",poi+i);
for(int i=1;i<n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
dfs1(1,1);
dfs2(1,1);
build(1,1,n);
scanf("%d",&m);
while(m--)
{
int k,x,y,val;
scanf("%d%d",&k,&x);
if(k==1)
{
scanf("%d%d",&y,&val);
update_path(x,y,val); //修改路径上的节点权值
}
else if(k==2)
{
scanf("%d",&val);
update_tree(x,val); //修改子树上的节点权值
}
else if(k==3)
{
scanf("%d",&y);
ll ans=query_path(x,y); //询问路径权值和
printf("%lld
",ans);
}
else if(k==4)
{
ll ans=query_tree(x); //询问子树权值和
printf("%lld
",ans);
}
}
return 0;
}
总结
树链剖分是一种优秀的算法。完美地体现了化繁为简的思想,为许多树上操作提供了便利。树剖码量很大,刚学完时调代码可能会很难受。但是熟练之后错误率就小很多了。