树链剖分新手正确的入门姿势 附带dfs序介绍 —— 详细证明一下一些结论

part one、dfs序/时间戳

dfs序就是按照树的先序遍历的顺序,为每个点记录下进入/最后一次出去这个点的时间。

dfs序是维护一个树基本套路之一,有一些基本的用处(蒟蒻我知道的):

1.树结构线性化,主要用于确定子树的范围。比如例题:

(银牌题)ACM-ICPC 2018 沈阳赛区网络预赛 J - Ka Chang dfs时间戳+树状数组+二分+分块(比较综合的题目)

2.树链的划分,树链剖分中用于将重节连续标号转化为重链。(下面会有讲)

3.别的蒟蒻就不会了。

就不贴代码了,参考上面给的题解链接里的AC代码。

part two、树链剖分

本篇重点是树链剖分。
百科上对树链剖分的描述:指一种对树进行划分的算法,它先通过轻重边剖分将树分为多条链,保证每个点属于且只属于一条链,然后再通过数据结构(树状数组、SBT、SPLAY、线段树等)来维护每一条链。

最经典的例子就是动态修改节点值,查询从某个节点到另一节点路径上点权和。

网上有大量的讲解,但对我这种蒟蒻来说非常新手不友好。那么我来总结一下原理。

先给一个例题HDU - 3966

题意:给一棵树,并给定各个点权的值,然后有3种操作:

I  C1 C2 K: 把C1与C2的路径上的所有点权值加上K

D C1 C2 K:把C1与C2的路径上的所有点权值减去K

Q C:查询节点编号为C的权值

数据量为5e4

(代码参考kuangbin的模板)

大家思考一下这个题怎么处理,然后下面进行详细讨论。

前驱知识点:dfs序、dfs、线段树/树状数组、前向星(大家都是用的前向星存图我也不知道为什么,我只好跟风了)

链式前向星 学习笔记

前缀和,线段树,树状数组讲解(入门)

树状数组 区间修改 区间查询 讲解

----------------------回到正题------------------

天生我才必有用?

线段树、主席树都是利用二叉树的性质。区间和树都是比较完美的。

可是,如果给出一棵树不是二叉树(可能是任意的树),而常规的树(二叉树)都是利用深度是节点个数/叶子个数的高阶无穷小(logn)来简化计算量的。而任意的树可能度非常大,也可能深度特别大,树链不完美,线段树家族处理某条子链的能力不强,则事情就麻烦了。

轻、重节点:兄弟节点中,子树结点数目最多的结点,其他的兄弟节点都为轻节点

重链:将连续的重节点连接在一起就是一条重链,最上面的重链的父亲也算在重链内。

轻链:除了重节点以外的轻节点就是轻链,长度必为1且为叶子,我们将会在下图后面详细分析。

树链剖分的核心思想是:重链是引起树复杂(深度过大)的原因,以重链为单位建立线段树维护(或者其他数据结构),其他轻节点暴力向重链转移。这样就相当于在树上建立了重链组成的高铁,轻节点转移到重链上之后就可以搭高铁了(搭高铁的意思是,转移到重链之后,重链只需要查询(修改)一次就可以查询(修改)到结果)。

请允许我画一个丑陋的

上图中红色为重节点,黑色为重链(也就是所谓的高铁),注意我们逻辑上重链要包含最上面的父亲。

有结论:

重链的个数不超过logn。

(网上说的是轻、重链的个数不超过logn,这至少在我们的定义中是错误的,举个反例就是深度为2,有100个叶子的树)

顺带把轻链必长度为1且为叶子一块解释了:

我们根据重链的定义可知:每个节点的孩子中必有一个重节点,这很好理解。而我们把重节点上面的父亲节点算作重链的开头第一个节点,所以:只要节点有子孙,则一定划归于重链,换句话说就是:不是叶子的节点,就是重链的一部分(要么是重节点,要么是重节点的父亲)。显然,不可能有两个叶子相连,则轻链长度只能为1.

重要的话再用红字写一遍,轻链长度为1。

证明重链的个数不超过logn:

(类似反证法)

    我们经过分析发现对于一个非叶子节点:要么它是重链,要么在重链最上端,也就是说对每个非轻链的节点都存在一条经过它或者从它开始,直达一个叶子的重链。那么要使重链尽可能的多,必须要使得树不断分叉(如果不的话就只有一条直达叶子的重链),分叉越多层数就越少,而我们知道每组兄弟中,只有一个是重节点,则分叉不能太多,那么就是二叉,而对于平衡二叉树最多logn级别的,这是重链最多的情况,普通情况重链自然就小于logn了。

为什么可以这么做?(复杂度分析):

我们树链剖分核心思想就是重链用数据结构维护,轻链尽可能搭重链便车来简化运算,而轻链长度最多为1。这样,轻链只需一次就可以到达重链,而重链个数不超过logn,所以重链之间的辗转也就不超过logn次(如下图黄色为轻链向根节点的移动轨迹),所以总体时间复杂度为logn。


重链如何成为快速的“高铁”?——dfs序+数据结构维护

再重复一遍之前得到的结论:轻链只需一次就能移动到重链,重链之间辗转不超过logn次

我们接下来分析重链为什么能、如何加速整个数据结构。也就是重链内部是如何传递的。

所以要回答这个问题,就是要回答如何用数据结构维护重链。比如例题是对链进行加减查询操作,所以我们选择树状数组(或者线段树)来维护重链。(再把例题贴一遍:)

例题HDU - 3966

题意:给一棵树,并给定各个点权的值,然后有3种操作:

I  C1 C2 K: 把C1与C2的路径上的所有点权值加上K

D C1 C2 K:把C1与C2的路径上的所有点权值减去K

Q C:查询节点编号为C的权值

数据量为5e4

(代码参考kuangbin的模板)

上面我们已经说了,轻链向重链靠拢,重链使用数据结构维护。要用树状数组维护重链,就要先将重链连续标号。也就是线性化。

这里使用dfs序将树上节点重新标号。从根节点出发,按照优先遍历重节点的顺序,先序遍历整棵树,记录下每个节点的时间戳。这样我们发现,由于是优先遍历重节点,则同一个重链内部一定是连续序号的。只要重链内部序号连续,就可以用树状数组维护区间特性了。

具体操作

当然不同题目要求,我们采取的数据结构维护也不一样,但有一些基本操作。以上述例题为例。

定义数据和初始化:

#include<bits/stdc++.h>
#include<cstring>
using namespace std; 
int const maxn=5e4+10;//数据量
/*              树的内存和结构               */ 
struct Edge{    //前向星数据结构(我按照邻接表理解的,下面我都按照邻接表来讲) 
    int to;           //邻接表条目 
    int next;       //指向下一个节点的指针。 
}edge[maxn*2];    //邻接表内存池 ,要开足够大。 
int head[maxn],tot;    //head是邻接表头指针。tot是内存池分配指针,初始为0;

/*            下面是节点、链的信息            */
int top[maxn];    //top[v] 记录节点v所在重链的顶端节点。顶端节点应为轻节点(重节点的父亲)
int fa[maxn];      //记录节点的父亲节点(前驱)
int deep[maxn];    //记录节点深度
int num[maxn];    //num[v]表示以v为根的子树节点数。 
int p[maxn];//p[v]表示v对应的位置(节点对应的dfs序)
int fp[maxn];//与p数组相反。(dfs序对应的节点号) 
int son[maxn];  //重儿子。

int pos; 

void init(){    //初始化
    tot=0;
    memset(head,-1,sizeof(head));
    pos=1;//使用树状数组,编号从1开始
    memset(son,-1,sizeof(son)); 

建立树:

首先我们要把树建立起来,这里选择用链式前向星(也就是内存池+邻接表)的方式存图。

/*不断添加边来建树*/
void addedge(int u,int v){     //头插法向u的邻接表里插入v,若无向图则要正反都添加 
    edge[tot].to=v;     //从内测池edge中取一个节点空间, 
    edge[tot].next=head[u];    //将节点插入邻接表,头插法 
    head[u]=tot++;    //将头节点重新指向链表头部,内存池计数变量加一 
}

初始化信息:

第一次dfs得到的信息有:每个点的(深度、父亲、子树点的个数、重儿子)

void dfs1(int u,int pre,int d){ //当前节点,前驱节点,深度 
    deep[u]=d;        //初始化深度
    fa[u]=pre;        //记录前驱
    num[u]=1;         //子树点个数统计,算上自己的1
    for(int i=head[u]; i!=-1 ;i=edge[i].next){    //遍历u的所有儿子
        int v=edge[i].to; //儿子
        if(v!=pre){
            dfs1(v,u,d+1);  //递归 
            num[u]+=num[v]; //加上儿子的子树节点个数
            if(son[u]==-1 || num[v]>num[son[u]]){ //寻找重儿子
                son[u] = v; 
            }
        }
    }
}

设置dfs序并将重节点串成重链:

/*第二次dfs优先遍历重节点设置dfs序,连接重链,并寻找每个节点(如果在重链上)所在重链的头部*/
void getpos(int u,int sp){    //当前节点,所在重链头部。 
    top[u]=sp;    //统计所在重链头部
    p[u]=pos++;    //记录节点号对应的dfs序 
    fp[p[u]]=u;    //记录dfs序对应的节点号 
    if(son[u] == -1)    //因为只有叶子没有重儿子,所以用来判断是否为叶子。 
        return;
    getpos(son[u],sp);//优先递归遍历重儿子,重儿子重链头部跟自己一样,所以直接填sp 
    for(int i=head[u] ; i!=-1;i=edge[i].next){    //遍历轻儿子 
        int v=edge[i].to; //轻儿子 
        if(v!=son[u] && v!=fa[u]){    //确保是轻儿子
            getpos(v,v); //轻儿子要么是轻链(轻链就不用管啦),要么是重链开头,所以sp填轻儿子本身。 
        }
    } 
}

至此树链剖分部分就完成了,接下来是用数据结构(树状数组)维护重链。

树状数组维护重链

(直接套一个裸的树状数组) 

注意:柱状数组是建立在dfs序上的。

前缀和,线段树,树状数组讲解(入门)<-------传送门

/*-------------------树状数组-------------------*/
#define lowbit(x) (x&-x)
int c[maxn];//树
int n;
int sum(int i){//求前缀和 
    int s=0;
    while(i>0){
        s+=c[i];
        i-=lowbit(i);
    }
    return s;
}
void add(int i,int val){//
    while(i<=n){
        c[i]+=val;
        i+=lowbit(i);
    }
}

题目解决部分

改变路径上点权

对于轻链直接暴力改变,对于重链使用树状数组区间更新。

/*                    解决题目                */
/*            u-->v的路径上点的值改变val      */
void change(int u,int v,int val){
    int f1=top[u],f2=top[v];//top是所在链起始端点(对于轻链就是本身喽) 
    int tmp=0;
    while(f1 !=f2){    //直到u和v辗转到同一个重链后停止。 一段一段change 
        if(deep[f1]<deep[f2]){    //为了方便,使f1深度大 也就是u移动次数多 
            swap(f1,f2);
            swap(u,v);
        }
        add(p[f1],val);    //由于u深度大,所以先让u往 lca靠 
        add(p[u]+1,-val);//这里的add是后缀区间加值,所以这一句把多加的后缀区间减掉,就变成了重链上一个区间。
        u=fa[f1];    //重链之间辗转 
        f1=top[u];    //重链之间辗转 
    } 
    if(deep[u]>deep[v])        //while结束之后u和v在同一重链上了,然后 把最后一段change掉 
        swap(u,v);
    add(p[u],val);        //两个点已经在同一个重链上了,直接区间改变即可。 
    add(p[v]+1,-val);
}

主函数

int a[maxn];
int main(){
    #ifndef ONLINE_JUDGE
    freopen("r.txt","r",stdin);
    #endif
    int m,q;
    while(scanf("%d%d%d",&n,&m,&q)!=EOF){
        int u,v;
        int c1,c2,k;
        char op[10];
        init();
        for(int i=1;i<=n;i++)
            scanf("%d",&a[i]);
        while(m--){
            scanf("%d%d",&u,&v);
            addedge(u,v);
            addedge(v,u);//无向图双向都要加 
        }
        dfs1(1,0,0);//根节点,根节点的father,根节点的深度
        getpos(1,1);//根节点,根节点所在重链起始节点。
        memset(c,0,sizeof(c));//树状数组清零
        for(int i=1;i<=n;i++){
            add(p[i],a[i]);
            add(p[i]+1,-a[i]);
        } 
        while(q--){
            scanf("%s",op);
            if(op[0]=='Q'){
                scanf("%d",&u);
                printf("%d\n",sum(p[u]));
            }
            else{
                scanf("%d%d%d",&c1,&c2,&k);
                if(op[0]=='D')
                    k=-k;
                change(c1,c2,k);
            }
        } 
    }

    

全部AC代码

#include<bits/stdc++.h>
#include<cstring>
using namespace std; 
int const maxn=5e4+10;//数据量
/*              树的内存和结构               */ 
struct Edge{    //前向星数据结构(我按照邻接表理解的,下面我都按照邻接表来讲) 
    int to;        //邻接表条目 
    int next;//指向下一个节点的指针。 
}edge[maxn*2];//邻接表内存池 ,要开足够大。 
int head[maxn],tot;//head是邻接表头指针。tot是内存池分配计数指针,初始为0;

/*            下面是节点、链的信息            */
int top[maxn];//top[v] 记录节点v所在重链的顶端节点。顶端节点应为轻节点(重节点的父亲)
int fa[maxn];//记录节点的父亲节点(前驱)
int deep[maxn];//记录节点深度
int num[maxn];//num[v]表示以v为根的子树节点数。 
int p[maxn];//p[v]表示v对应的位置(节点对应的dfs序)
int fp[maxn];//与p数组相反。(dfs序对应的节点号) 
int son[maxn];//重儿子。

int pos; 
void init(){
    tot=0;
    memset(head,-1,sizeof(head));
    pos=1;//使用树状数组,编号从1开始
    memset(son,-1,sizeof(son)); 

/*添加边*/
void addedge(int u,int v){     //头插法向u的邻接表里插入v,若无向图则要正反都添加 
    edge[tot].to=v;     //从内测池edge中取一个节点空间, 
    edge[tot].next=head[u];    //将节点插入邻接表,头插法 
    head[u]=tot++;    //将头节点重新指向链表头部,内存池计数变量加一 
}
void dfs1(int u,int pre,int d){ //当前节点,前驱节点,深度 
    deep[u]=d;        //初始化深度
    fa[u]=pre;        //记录前驱
    num[u]=1;         //子树点个数统计
    for(int i=head[u]; i!=-1 ;i=edge[i].next){    //遍历u的所有儿子
        int v=edge[i].to; //儿子
        if(v!=pre){
            dfs1(v,u,d+1);  //递归 
            num[u]+=num[v]; //加上儿子的子树节点个数
            if(son[u]==-1 || num[v]>num[son[u]]){ //寻找重儿子
                son[u] = v; 
            }
        }
    }
}
/*第二次dfs优先遍历重节点设置dfs序,连接重链,并寻找每个节点(如果在重链上)所在重链的头部*/
void getpos(int u,int sp){    //当前节点,所在重链头部。 
    top[u]=sp;    //统计所在重链头部
    p[u]=pos++;    //记录节点号对应的dfs序 
    fp[p[u]]=u;    //记录dfs序对应的节点号 
    if(son[u] == -1)    //因为只有叶子没有重儿子,所以用来判断是否为叶子。 
        return;
    getpos(son[u],sp);//优先递归遍历重儿子,重儿子重链头部跟自己一样,所以直接填sp 
    for(int i=head[u] ; i!=-1;i=edge[i].next){    //遍历轻儿子 
        int v=edge[i].to; //轻儿子 
        if(v!=son[u] && v!=fa[u]){    //确保是轻儿子
            getpos(v,v); //轻儿子要么是轻链(轻链的起始就是本身喽),要么是重链开头,所以sp填轻儿子本身。 
        }
    } 
}

/*-------------------树状数组-------------------*/
#define lowbit(x) (x&-x)
int c[maxn];//树
int n;
int sum(int i){
    int s=0;
    while(i>0){
        s+=c[i];
        i-=lowbit(i);
    }
    return s;
}
void add(int i,int val){
    while(i<=n){
        c[i]+=val;
        i+=lowbit(i);
    }
}

/*                    解决题目                */
/*            u-->v的路径上点的值改变val      */
void change(int u,int v,int val){
    int f1=top[u],f2=top[v];//top是所在链起始端点(对于轻链就是本身喽) 
    int tmp=0;
    while(f1 !=f2){    //直到u和v辗转到同一个重链后停止。 一段一段change 
        if(deep[f1]<deep[f2]){    //为了方便,使f1深度大 也就是u移动次数多 
            swap(f1,f2);
            swap(u,v);
        }
        add(p[f1],val);    //由于u深度大,所以先让u往 lca靠 
        add(p[u]+1,-val);//这里的add是后缀区间加值,所以这一句把多加的后缀区间减掉,就变成了重链上一个区间。
        u=fa[f1];    //重链之间辗转 
        f1=top[u];    //重链之间辗转 
    } 
    if(deep[u]>deep[v])        //while结束之后u和v在同一重链上了,然后 把最后一段change掉 
        swap(u,v);
    add(p[u],val);        //两个点已经在同一个重链上了,直接区间改变即可。 
    add(p[v]+1,-val);
}

int a[maxn];
int main(){
    #ifndef ONLINE_JUDGE
    freopen("r.txt","r",stdin);
    #endif
    int m,q;
    while(scanf("%d%d%d",&n,&m,&q)!=EOF){
        int u,v;
        int c1,c2,k;
        char op[10];
        init();
        for(int i=1;i<=n;i++)
            scanf("%d",&a[i]);
        while(m--){
            scanf("%d%d",&u,&v);
            addedge(u,v);
            addedge(v,u);//无向图双向都要加 
        }
        dfs1(1,0,0);//根节点,根节点的father,根节点的深度
        getpos(1,1);//根节点,根节点所在重链起始节点。
        memset(c,0,sizeof(c));//树状数组清零
        for(int i=1;i<=n;i++){
            add(p[i],a[i]);
            add(p[i]+1,-a[i]);
        } 
        while(q--){
            scanf("%s",op);
            if(op[0]=='Q'){
                scanf("%d",&u);
                printf("%d\n",sum(p[u]));
            }
            else{
                scanf("%d%d%d",&c1,&c2,&k);
                if(op[0]=='D')
                    k=-k;
                change(c1,c2,k);
            }
        } 
    }

 

猜你喜欢

转载自blog.csdn.net/GreyBtfly/article/details/82926675