您的位置:首页 > 其它

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

2016-01-29 23:51 211 查看
前几天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)软件包管理器那道题的代码有点小错,在这里不贴出来丢人了……树链剖分的模板有上面那四个就没问题。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: