最近在做运输计划这道题时,发现要用数链剖分,于是就打算学学这个玩意儿。
其实之前一直以为这个东西是个很复杂的东西,可能代码看起来都很长。
但是,学了之后,我才发现,这个东西很好懂,而且代码之所以很长,也有一个原因就是它需要使用一个强大的数据结构——线段树。线段树的代码其实并不算少,这个,学过的人,都应该知道,没学过的人,不妨看一下:线段树
所以,我称之为狐假虎威。
那么,下面就让我为你们揭开数链剖分的真实面目。
图的存储方法
我们在这里我们采用边的结构体和链表去存储。
先写一个结构体:
struct edge{
int from,to,w,next;//from是始结点,to是终结点,w是权值,next是链表中下一个结点
}
这就是边的结构体。
注意:如果next为空,则为0
下面我们再定义所需的变量。
edge edg[maxm]; //边,maxm是边的最大值
int head[maxn]; //链表的头,maxn是点的最大值,表示对应结点指向的第一个边的编号
这就是我们对于图的表示。
这个是基础的东西,我们不多说。
数链剖分主体
构造
下面是数链剖分的构造环节,我们先来看看我们需要构造哪些数据。
所需数据
int siz[maxn];//以该结点为根结点的子树的结点数
int top[maxn];//该结点所在重链的头结点
int son[maxn];//该结点的重儿子(初始值-1)
int dep[maxn];//该结点深度
int faz[maxn];//该结点的父结点(初始值-1)
int tid[maxn];//该结点的dfs序
int rnk[maxn];//dfs序对应的结点编号
int w[maxn];//点的权值
你可能一下就看懵了,这些都是什么意思啊?
别急,下面我来解释一下概念:
- 重儿子(重结点):对于一个结点来说,其子结点中,子树结点数最多的子结点是重儿子
- 轻儿子(轻结点):对于一个结点来说,其子结点中,非重结点的子结点是轻结点
- 重边:连接两个重结点的边是重边
- 轻边:连接两个轻结点的边是轻边
- 重链:多个重边组成的链
- 轻链:多个轻边组成的链
- 重链的头结点:就是一条重链中深度最小的结点
其他的,至于什么是dfs序,什么是深度,大家应该知道。(如果不知道,那么少年(或妹子),我劝你先去学一学基础)
接下来我们就要构造这些数组了。
第一次dfs构造
你应该不会问我什么是dfs吧?那么我会告诉你是深度优先遍历。
如果你真的不知道,那么自行百度,下面不会对大dfs进行介绍的。
本次dfs的目标是构造dep[]、siz[]、son[]、faz[]、w[]。
代码如下:
void dfs1(int u,int father,int depth){//u是当前结点,father是u的父结点,depth是当前深度
siz[u]=1;//包含本身
son[u]=-1;//-1表示目前没有设置重儿子
faz[u]=father;//直接保存父结点
dep[u]=depth;//设置深度
for(int i=head[u];i;i=edg[i].next){
int v=edg[i].to;//取出相连的结点
if(v!=father){//如果不是父结点
dfs1(v,u,depth+1);//继续遍历
w[v]=edg[i].w;//这里是将每一条边的权值转换到儿子上
siz[u]+=siz[v];//加上以v为根结点的子树的结点数
if(son[u]==-1||siz[v]>siz[son[u]]){//判断是否需要更新重儿子
son[u]=v;
}
}
}
}
中间我对于边上权值进行了一个转换,如果题目给的就是点权值,那么就需要转换了,但如果题目给的边权值,那么我们就转换成点权值,也就是将每一条边的权值放在儿子上,而不是放在父亲上。(自己想想为什么)
这个过程是很好理解的,我不过多解释了。
第二次dfs构造
这次dfs的目的,就是构造剩下的三个数组,即top[]、tid[]、rnk[]。
代码如下:
void dfs2(int u,int t){//t重链的头结点,u表示当前结点
top[u]=t;//直接保存重链头结点
tid[u]=cnt;//保存dfs序
rnk[cnt]=u;//保存dfs序对应的结点
cnt++;//dfs序递增
if(son[u]==-1){//无儿子,不处理
return;
}
dfs2(son[u],t);//遍历重儿子
for(int i=head[u];i;i=edg[i].next){
int v=edg[i].to;//取连接的结点
if(v!=son[u]&&v!=faz[u]){//如果该结点不是重结点和父结点
dfs2(v,v);//把这个子结点的链头结点设置为自己,因为它和当前结点之间是轻边
}
}
}
也很好理解,不多说,只说一个注意点,那就是我们一定是先遍历重儿子,再遍历轻儿子,这个大家自己先想想为什么。(画个图就知道了,和线段树有关,讲线段树的时候再讲)
操作
我们这里只进行两种操作,更多的操作还要靠各位自己去举一反三。
我们提供的两个操作是两个最基本也是最经典的操作——查询x到y的距离、给x到y的每一个边的权值加上z。
其实就是线段树的区间修改和区间和查询。
当时我们先讲讲数链剖分这一块的操作。
查询x到y的路径长度
学过的人都知道,就是一个LCA(最近公共祖先)。
思路是,每一次,将深度大的一个结点,运用重链头结点向上调整,最后找到公共祖先。
代码如下:
//查询x到y的最短路径长度
int query_path(int x,int y){
int fx=top[x],fy=top[y];//取链头
int ans=0; //初始化答案
while(fx!=fy){//如果两者的链头不同,则继续调整
if(dep[fx]>=dep[fy]){//比较哪一个深度更大,调整深度更大的一个
ans+=query(1,n,1,tid[fx],tid[x]);//这里显然是线段树的query,功能是计算两个结点间的距离
x=faz[fx];//将x调整为fx的父结点
}else{
ans+=query(1,n,1,tid[fy],tid[y]);//和上面对应
y=faz[fy];
}
fx=top[x],fy=top[y];//取当前两个结点的链头
}
if(x!=y){//如果两个不相等,说明还有一段距离没有计算,因为前面只保证他们的链头相等
if(tid[x]<tid[y]){//依据编号大小计算剩下的距离
ans+=query(1,n,1,tid[x],tid[y]);
}else{
ans+=query(1,n,1,tid[y],tid[x]);
}
}else{
ans+=query(1,n,1,tid[x],tid[y]);
}
return ans;
}
这里调整还是比较清晰的,但是可能有的人会有一个疑问,就是调整为父亲的时候,会不会调整出根节点的父亲?
其实是不会的,因为我们每次都调整深度更深的那一个,所以不可能是根节点,这样就不会取到根节点的父结点。
将x到y的一段路上每一条边都加上z
这是经典的修改操作。
代码和查询惊人的相似。
代码如下:
void update_path(int x,int y,int z){
int fx=top[x],fy=top[y];
while(fx!=fy){
if(dep[fx]>=dep[fy]){
modify(1,n,1,tid[fx],tid[x],z);
x=faz[fx];
}else{
modify(1,n,1,tid[fy],tid[y],z);
y=faz[fy]
}
fx=top[x],fy=top[y];
}
if(x!=y){
if(tid[x]<tid[y]){
modify(1,n,1,tid[x],tid[y],z);
}else{
modify(1,n,1,tid[y],tid[x],z);
}
}else{
modify(1,n,1,tid[x],tid[y],z);
}
}
好像没什么可讲的,和查询差不多。
数据结构(线段树)
接下来要讲数链剖分的数据结构基础。
当然不见得都要用线段树,你也可以用树状数组、splay之类的数据结构,这里用线段树做一个例子,如果你没学过线段树,你可以看看这篇写得不错的文章:线段树
这次不直接放代码,我们先讲讲之前留下的那个问题。
那就是:为什么先遍历重儿子?
那么请你拿出笔和纸,画一棵树,然后先描出重链,然后按优先遍历重儿子,给它们标上dfs序,然后,你再模拟一下我们的查询过程。
你有什么发现?
每次求和的几个点的dfs序都是连续的。这样的话我们就能用各种数据结构去实现。(因为我们的数据结构基本上都是求区间和,很少跳跃性求和的)
下面奉上全套线段树代码:
void update(int rt)
{
sum[rt]=sum[rt<<1]+sum[rt<<1|1];
}
void color(int l,int r,int rt,int a) {
sum[rt]=sum[rt]+a*(r-l+1);
add[rt]+=a;
}
void push_col(int l,int r,int rt) {
if (add[rt]) {
int m=(l+r)>>1;
color(l,m,rt<<1,add[rt]);
color(m+1,r,rt<<1|1,add[rt]);
add[rt]=0;
}
}
void build(int l,int r,int rt){
if (l==r) {
sum[rt]=w[rnk[l]];//注意我们处理的rnk在这里派上用场了
add[rt]=0;
return;
}
int m=(l+r)>>1;
build(l,m,rt<<1);
build(m+1,r,rt<<1|1);
update(rt);
}
void modify(int l,int r,int rt,int nowl,int nowr,int c) {
if (nowl<=l && r<=nowr) {
color(l,r,rt,c);
return;
}
push_col(l,r,rt);
int m=(l+r)>>1;
if (nowl<=m) modify(l,m,rt<<1,nowl,nowr,c);
if (m<nowr) modify(m+1,r,rt<<1|1,nowl,nowr,c);
update(rt);
}
int query(int l,int r,int rt,int nowl,int nowr) {
if (nowl<=l && r<=nowr) return sum[rt];
push_col(l,r,rt);
int m=(l+r)>>1,ans=0;
if (nowl<=m) ans+=query(l,m,rt<<1,nowl,nowr);
if (m<nowr) ans+=query(m+1,r,rt<<1|1,nowl,nowr);
return ans;
}
注意这里的sum[]和add[]需要四倍的空间。
最后唠叨一句,它的时间复杂度是O(nlogn)。
结束语
那么,狐假虎威的数链剖分就告一段落了,其实挺简单的对吧?(不要被它的长度吓到了)
今天就写到这,回头会发布例题的,敬请关注!