树链剖分(一)(#请配合树链剖分(二)以及线段树一起食用-_-#)

前几天LJQ给我们一群蒟蒻讲了树链剖分,虽说软件包管理器那道题还是没有A掉,不过UOJ的数据已经A掉了。
其实树链剖分自己早就会了的……
相关定义:
size:某节点所在的子树大小,sz[x] = (for every child in x)sigma(sz[son]) + 1;
重儿子:在节点x的孩子中sz值最大的儿子。
重边:连接当前节点x与其重儿子的边。
轻边:除了重边以外的边。
重链:重边组成的简单路径。
相关性质:
1.size[x] > 2 * size[light_son] ;
证明:假设size[x] <= 2 * size[light_son],必然有sz[x] <= 2 * sz[heavy_son];
显然重儿子和轻儿子的size和超过了size[x],可见假设不成立。
2.从根到某一点的路径上轻链、重链的个数都不大于logn。
证明:我蠢……不会证……回去翻一下算导。
具体实现
首先我们要明确我们现在会的一些基础的数据结构(线段树)的适用条件是仅限于一条链的。那么我们如果想把这个数据结构应用到树上,就要对树进行一些改造,让它重新变成一条链,并且对结果的正确性有保障。
怎么做呢?我们可以对这整棵树进行深度上的划分,然后一个一个按深度串成一条链,虽然也能保证正确性,但是深度太深之后会爆炸。
在这里要用到的就是轻重边划分。
我们对整棵树先进行一遍dfs(随意dfs),这遍dfs过后,我们会记录它的父亲,深度,子树大小(size),并记录它的重儿子(如果有)。
然后再进行一遍dfs并维护一个时间戳tim,(dfs搜的时候优先搜它的重儿子,第一次回溯的时候搜轻儿子,第二次回溯的时候什么操作也不做(如果不懂可以看一下代码)),此时假设我们遍历到x节点,那么我们肯定知道:
1.x——heavy_son是一条重边。
2.x——light_son肯定在heavy_son之后搜到。
3.也就意味着,在dfs序中,每个点的heavy_son一定出现在light_son之前。
4.dfs结束后,形成的dfs序是几条重链一个一个“串”起来的,其中有一些点是重链的链顶,即它们不是某个点的重儿子。
我们看了看第四条,发现只有第四条说的好像不是废话。于是,想到了自己曾经写过的倍增LCA,发现其实针对树上的路径,如果两个点不在同一条路径,那么就让链顶比较深的点直接跳到重链的顶端,可以知道,链顶一定是它父亲的轻儿子,这样之后跳到链顶的父亲上,就可以到达另一条重链。这样跳来跳去,有缘千里来相会,它们肯定能跳到一条重链上。
好了,我们发现一个事情,就是树链剖分可以求LCA
①这样的话,我们如果出道题在某个点到某个点的路径搞来搞去,我们显然就知道了这道题可能跟树链剖分有关。
来个题试试:求树上A——B的路径权值最大值。
对这棵树进行树链剖分,搞一个大的线段树维护所有重链串起来的最大值,之后,我们对于A——B不在一条重链的情况中,每次取链顶深度较大的那个点,并找出这一段在线段树里的最大值,最后所有的最大值取个max即可。
请注意如下的部分:
我初学树链剖分时,想法是这样的:“那么麻烦干啥啊,赶紧直接求出LCA然后直接在线段树上询问A——LCA的时间戳,B——LCA的时间戳不就得了吗?”
我这么厉害怎么不上天呢……按照我的求法,倒不如直接在树上询问A——B的时间戳来得直接。在这里声明一下:我们在找A——B的路径并把它对应在这个数据结构上的时候,我们是按照dfs序来走的。
dfs序!dfs序!dfs序!
而且不是一般的dfs序哦……是优先dfs重儿子的dfs序哦……(强行卖萌)
(其实这好像不叫dfs序了)
②其实我们不仅可以在路径上搞,还能在子树上搞……
想一想,在第二遍dfs给x盖时间戳的时候,什么时候这个x节点算是彻彻底底被访问过了?事实上,它完成了它的第一次搜索重链,和第二次搜索轻边之后,回溯回到了x,此时,x就彻底完成了它的使命。
让我们回一下魂,会发现,在搜到x到x完成它的使命这段时间内,实际上程序只走了x的子树。也就是,时间戳在这一段时间仅仅是给x的子树中的所有节点打了标记。
这样的话,对应到维护的那个数据结构(线段树)上的话,tim(x) ~tim(x + size[x] - 1)这一段区间,维护的就是它的子树。
至此,树链剖分的基本原理,具体应用已经分析完毕,关于它的时间复杂度O(log^2 n),其实我想说:在NOI的考题中,你看出这道题是树链剖分,好了开搞吧,肯定能过。
变量/数组含义
fa[]:记录该节点的父亲。
size[]:统计子树大小。
dep[]:深度。
tim:全局变量时间戳。
son[]:统计重儿子。(初值:-1)
top[]:重链顶端。
tid[]:每个点的时间。
Rank[]:(请注意这里一定要大写,不然容易冲突)每个时间对应的是哪个节点,可记可不记。

void dfs1(int rt,int f,int depth){//找出重儿子
    fa[rt]=f;
    dep[rt]=depth;
    size[rt]=1;
    for(int i=head[rt];~i;i=edge[i].next){
        int v=edge[i].to;
        if(v!=f){
            dfs1(v,rt,depth+1);
            size[rt]+=size[v];
            if(son[rt]==-1||size[son[rt]]<size[v])
                son[rt]=v;
        }
    }
}
void dfs2(int rt,int tp){//连重边成重链
    top[rt]=tp;
    tid[rt]=++tim;
    Rank[tid[rt]]=rt;
    if(son[rt]==-1)return;
    dfs2(son[rt],tp);
    for(int i=head[rt];~i;i=edge[i].next){
        int v=edge[i].to;
        if(v!=fa[rt]&&v!=son[rt])
            dfs2(v,v);
    }
}
#define lc rt<<1,l,mid
#define rc rt<<1|1,mid+1,r
void build(int rt,int l,int r){//建树。(该怎么建还怎么建)
    if(l==r){
        sum[rt] = w[Rank[l]];//不一定是sum……就是节点信息
        lazy[rt]=-1;//etc.
        return;
    }
    int mid=(l+r)>>1;
    build(lc);build(rc);
    pushup(rt);
}
void change(int a,int b){//从一条重链跳到另一条。
    int s=0,num=dep[b];
    while (top[a]!=top[b]){
        if (dep[top[a]]<dep[top[b]])swap(a,b);
        s+=query(1,1,n,tid[top[a]],tid[a]);
        a=fa[top[a]];
    }
    if (dep[a]>dep[b]) swap(a,b);
    s+=query(1,1,n,tid[a],tid[b]);
  }

请注意,本人的NOI2015(还是14)软件包管理器那道题的代码有点小错,在这里不贴出来丢人了……树链剖分的模板有上面那四个就没问题。

你可能感兴趣的:(树链剖分(一)(#请配合树链剖分(二)以及线段树一起食用-_-#))