树链剖分小结及题目

来源:互联网 发布:阿里云试用服务器 编辑:程序博客网 时间:2024/06/10 19:56

树链剖分小结及题目

         我们经常会遇到这样一类题目:给出一棵树,询问树上u,v两点路径间的最值,合值,更新uv路径上的点权或边权,或者区间更新etc,此时如果单纯的用线段树或者树状数组去搞,很明显问题不能够得到完美解决,此时就需要更高级的数据结构去对树进行重新构造,也就是通常说的树链剖分。

一.树链剖分

        树链剖分,顾名思义,也就是对树的每一条链进行剖分,将一棵树拆分成若干条链,对其进行重新编号,在进行了重新编号之后就可以用其他数据结构(线段树,树状数组,etc)去对数据进行有序化处理,这也就是说,大部分情况下,树链剖分是在为其他数据结构“打辅助”,和其他数据结构搭配起来使用才能更充分发挥它的作用。

剖树

首先我们引入一些新的符号标记和一些概念

 

siz[i]:i
son[i]:isiz
Deep[i]:i
fa[i]:i
top[i]:i

 

siz[u]vsizuv
v
v
v


 

对于一棵树来说,为了让他有序化,我们进行这样的操作:

1.找到每个结点的重儿子

2.将每个结点与重儿子组成重边,与其它结点组成轻边

3.对每个结点按重链深搜,轻边其次,进行重新编号映射

经过这样的操作之后,可以发现一棵树就被分为了重链和轻边,并且结点与结点之间要么是在同一条重链上,由重边相连接,要么不在同一个重链上,由重边和轻边交替连接。

 

代码实现:

第一步dfs 首先处理出fadeepsizson ,这样预处理是为了在第二个dfs中直接沿着重儿子进行编号以处理重链编号。


void dfs1(int k,int pre,int d){    deep[k]=d;    fa[k]=pre;    siz[k]=1;    for(int i=fir[k];~i;i=nex[i])    {        int e=v[i];        if(e!=pre)        {            dfs1(e,k,d+1);            siz[k]+=siz[e];            if(son[k]==-1||siz[son[k]]<siz[e]) son[k]=e;        }    }}

第二个dfs处理重链并对其进行编号,同时记录每个结点的top

void dfs2(int k,int sp){    top[k]=sp;    in[k]=tot++;    if(son[k]==-1) return;    dfs2(son[k],sp);    for(int i=fir[k];~i;i=nex[i])    {        int e=v[i];        if(e!=fa[k]&&e!=son[k])        {            dfs2(e,e);        }    }}

解释一下这样剖树的合理性:

        首先这样剖完树后我们很容易观察到两个性质

        性质1:如果(v,u)为轻边,则siz[u] * 2 < siz[v]
        性质2:从根到某一点的路径上轻链、重链的个数都不大于logn

        现在考虑树上两点uv的路径问题由于子节点到父节点的路径是唯一的,而父节点到子节点的路径不唯一,我们让两点都到LCA上去,我们尝试让两点都到达LCA,根据以上的两个性质,我们最多经过logn条链就能让两个点在同一条重链上,这也就意味着我们仅仅花费了logn的复杂度就遍历了uv的所有路径!!!问题就较完美的解决了。(下面会详细讲解爬树过程)

        对一棵树进行剖分编号之后,还可以观察到:从根节点向下对所有结点的重链进行编号,的对于一条重链来说,其编号一定是连续的,对于一个结点来说,其子树的编号一定是连续的。

一棵树在剖完之后是这个样子:

 

红色代表重链,青色代表轻边,结点的编号是对链的重新编号。

这样剖完之后发现,对于重链来说完全可以将它映射到线段树上,这样就可以区间的去解决树链的问题,降低复杂度。


LCA

在找两点路径时,运用一种爬山的做法,每次将重链较深的结点优先向上爬,这样一直到两个结点都到达同一条重链上,相当于是在求LCA

比如现在找47LCA,那么路径是这样的:7->6->1,4->1

具体看代码

代码实现:

long long Query(int s,int t){    long long sum=0;    int f1=top[s],f2=top[t];    while(f1!=f2)//使s,t向上爬到同一条重链    {        if(deep[f1]<deep[f2]) swap(f1,f2),swap(s,t); //深度较低的优先向上爬        sum+=query(in[f1],in[s],1,tot-1,1);//统计答案        s=fa[f1];        f1=top[s];    }    if(deep[s]>deep[t]) swap(s,t);    sum+=query(in[s],in[t],1,tot-1,1);///此时已经在同一条重链上了    return sum;}

        这里的向上爬是十分巧妙的,每一次让top较深的结点找到当前重链上的顶端结点直接跳跃向上爬,由于重链的编号又是连续的,所以又可以在线段树上直接进行区间统计,对于整颗树来说,相当于是logn的向上爬,对于统计答案来说,又是logn的在统计答案。试想如果每一次直接找每个结点的fa向上爬会怎样,很明显,复杂度大大超出,如果不让较深的结点向上爬会怎样,很明显,两点停在原链。

 

至此,树已经全部剖完,剩下的就是其他数据结构的操作了。

 

二.常见题型

Q1:给出一棵树,更新u->v路径上的所有点的权值,单点查询树上某点的权值

 

基于点权的树链剖分,在树链剖分处理完整颗树之后直接将结点编号,映射到树状数组或者线段树上处理查询修改。

 

HDU 3966 Aragorn's Story


题解:

HDU 3966 Aragorn's Story (树链剖分 基于点权)

Q2:给出一棵树,更新结点u的权值,查询u->v路径上的权值和

 

基于边权的树链剖分,对于边来说,除了根节点外,其他所有的边都可以与其子节点一一对应,这样就转换为了点权问题,在处理时要注意LCA当前点不统计,因为此时的边权不会被访问到。

 

SPOJ QTREE

POJ 2763 Housewife Wind

HYSBZ 1036 树的统计Count

FZU 2082 过路费

LightOJ 1348 Aladdin and the Return Journey

题解:

SPOJ QTREE (树链剖分 基于边权)

POJ 2763 Housewife Wind (树链剖分)

HYSBZ 1036树的统计Count(树链剖分 + 线段树)

FZU 2082过路费(树链剖分)

LightOJ 1348 Aladdin and the Return Journey (树链剖分)

 

Q3:给出一棵树,更新u->v路径上的权值,询问u->v的权值

 

还是基于边权,但是此时就需要加上线段树的延迟标记处理

 

POJ 3237 Tree

题解:

POJ 3237 Tree(树链剖分 线段树区间标记)

 

Q4:给出一棵树,更新点u,询问u->vLCIS

 

这类问题需要用到线段树的区间合并,写起来有些恶心,一些还是方向必须固定的,但是基本都有通用的方法:在进行向上爬区间时开两个结构体合并向上爬的区间合并结果,这样就得到了两个相向的结构体,那么答案要么就在两个结构体中,要么是两个结构体左端点进行合并的结果(这里需要手动处理),如果题目要求方向一定是u->v,那么就直接设置一个方向标记flag,判断爬上来的方向在哪个结构体中。

 

HYSBZ 2243 染色

HDU 5052 Yaoges maximum profit

HDU 4718 The LCIS on the Tree


题解:

HYSBZ 2243染色(树链剖分 + 线段树区间合并)

HDU 5052 Yaoge’s maximum profit (树链剖分+ 线段树)

HDU 4718 The LCIS on the Tree (树链剖分+ 线段树区间合并)

Q5:给出一棵树,修改u到根节点路径上权值,修改u子树的权值,询问u到根节点权值,询问u子树权值

对于u到根节点,这个很好做,直接向上爬,对于子树呢,注意上文提到过树链剖分之后对于一个结点u来说它的子树区间一定是连续的,那么基于这一点,我们用两个数组记录结点的区间,就能达到查询修改子树的目的

BZOJ 4034 T2

BZOJ 4196 软件包管理器

题解:

BZOJ 4034 T2 (树链剖分解决子树问题)

BZOJ 4196软件包管理器(树链剖分子树)

Q6:还有一些其他类型的题目,建模,动态开点,离线,递归pushdown之类的,方法比较巧妙。

uva 12655

HDU 5029 Relief grain

BZOJ 3531

HDU 3801 Query on a tree

题解:

uva 12655 (树链剖分+生成树 好题)

HDU 5029 Relief grain(恶心的树链剖分+ 线段树)

BZOJ 3531 (树链剖分,线段树动态开点)

HDU 3801 Query on a tree(树链剖分离线处理)

三.结语:

但凡涉及到树链剖分的题目,代码量往往比较大,在写的时候要注意思路一定要清晰,常考的题目尤其是树链剖分+线段树区间合并这一块,动辄开8个变量去进行区间合并,这样的题目一定要考虑清楚在动手,在得到两个相向结构体的时候更要考虑清楚边界如何处理。树链剖分能够和很多数据结构结合起来使用,当然也还有更多的好题和方法等待着去挖掘。

 

 

 

0 0