狐假虎威的树链剖分

最近在做运输计划这道题时,发现要用数链剖分,于是就打算学学这个玩意儿。
其实之前一直以为这个东西是个很复杂的东西,可能代码看起来都很长。
但是,学了之后,我才发现,这个东西很好懂,而且代码之所以很长,也有一个原因就是它需要使用一个强大的数据结构——线段树。线段树的代码其实并不算少,这个,学过的人,都应该知道,没学过的人,不妨看一下:线段树
所以,我称之为狐假虎威。
那么,下面就让我为你们揭开数链剖分的真实面目。

图的存储方法

我们在这里我们采用边的结构体和链表去存储。
先写一个结构体:

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)。

结束语

那么,狐假虎威的数链剖分就告一段落了,其实挺简单的对吧?(不要被它的长度吓到了)
今天就写到这,回头会发布例题的,敬请关注!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值