算法学习之:动态树(link-cut-tree)及bzoj3282Tree例题详解

来源:互联网 发布:java快速开发平台 开源 编辑:程序博客网 时间:2024/06/06 21:39

算法学习之:动态树(link-cut-tree,下文简称lct)

前言:经过大神对lct的各种狂吹之后,作为蒟蒻一只的我就来学习lct了,%了几份博客之后,大概明白了lct是怎么做。发现其实lct好像并没有想想中的那么难。当然lct的最低门槛是splay,其次当然就是树链剖分。当然理论上来说树链剖分不看的话lct其实可学,但是可能光是理解就要好久。所以想学lct的小伙伴们还是先去学学splay和树剖咯。

从一道简单题说起:

题目链接:戳这里

题目大意:森林上各种操作,包括森林树上的路径询问,拆分,合并和单点修改。

显然,如果说把拆分以及合并去掉的话,这道就是一道非常裸的树剖题。但是,树剖成立的前提条件就是树的形态是不变的,而显然,在森林中树的拆分与合并使得树的结构是不定的,因此我们希望有某种数据结构,支持树链的拆分合并,并且复杂度是nlog级别的,这就是动态树。

几个定义:

Access:如果这个点刚刚被访问过,那么称这个节点刚刚被执行了Access操作。

PreferredChild:在节点u的子树中,如果最后一个被访问的节点v在当前结点u的w子树中,那么称w为u的Preferred Child,如果节点u本身就是最后一个被访问的节点,那么u没有Preferred Child。

延伸定义:Preferred Edge和Preferred Path,分别表示Preferred Child的父边以及以其形成的路径。

接下来闭上眼睛,把这个几个概念回顾一下,直到你能把他的定义背出来。并且类比一下之前其他的算法概念。然后你会发现:超级像树剖的做法,只不过这棵树的Preferred Child不一定是子树节点个数最多的,因为这棵树的结构会随时变化,下文会提到。

数据结构登场——平衡树

由上面的操作,我们得到了若干条Preferred Path,对于每条Preferred Path,我们以深度为关键字①,维护一棵平衡树(当然是选择Splay,因为要支持转来转去的操作),顺带说一句,这个平衡树有个好听的名字,叫做Auxiliary Tree(辅助树)然而这个概念并不用背下来。但是你只要记住,这棵平衡树以深度为关键字②,是的以深度为关键字③

一个小操作:

假设我们现在已经维护出来了这些Auxiliary Tree,也就是很多很多棵splay,这个时候,我们为了方便,把每科splay的根的父亲,定义为这条Preferred Path在原树上最高节点对应的父亲节点。那么久延伸出了下面这个小操作,Is root。

Isroot:判断一个节点是否为一棵splay树的根。只要它父亲的左右子树的儿子都不是它,那么显然它就是子树的根。

bool Isroot(int p) {return t[pa].ch[0] != p && t[pa].ch[1] != p;} 

别看这个操作小,这个操作在接下来的Access中会有大用处。

最最关键的操作——Access

如果没学过lct,但是英文好的同学们,access可以被中文翻译成入口,进入,通道,访问等等,但是学了lct的人就知道,可以把lct翻译成“废嫡立庶”。(没错这是我瞎说的)

Access的目的就是:如果Access(v)那么我们要让v到树根节点的路径成为一条PreferredPath,而且这个节点是Preferred Path的最深的节点。那么原本v到根的路径上可能很多条路径都不是Preferred Edge,也就是说这路径上的很多点并不是它父亲的Preferred Child,那我们怎么办呢?正确的做法是,管他去死,直接把这个点变成它父亲的Preferred Child,其他的节点自然就不是Preferred Child了。为了形象一点,我把Access前后的状态画出来


这是Access之前的状态,红色是Preferred Path,黑色是其他路径


这是Access(u)之后的状态

那么如何操作呢。

首先考虑第一次操作,显然,如果Access的节点在某条Preferred Path上,那么我们就先要把这个节点和它的儿子们断开。那么就是转到跟,把右子树清零即可。至于为什么要这么做,下文会阐述

然后我们直接跳掉链头的父亲。这个操作怎么实现?我们之前把Splay的根的父亲定义为在原树上链头的父亲,那么这个时候,直接把这个节点转到根,然后跳到父亲就完事儿了。

其次,到了父亲之后,显然我们需要改变Preferred Child。怎么改?暴力改。因为改完之后,其它的儿子照样存在于自己的Auxiliary Tree中,根本不用管。

综合上述两个操作,得出代码。

void Access(int p) {    for(int pre = 0; p; pre = p, p = pa) {        Splay(p);        t[p].ch[1] = pre;        update(p);    }}

这一步的作用一定要理解清楚来。因为接下来的操作都是基于这个操作之上的。因此如果没有看懂,一定多看几遍。

两个辅助操作——makeroot和find

Makeroot

在某些情况下,我们需要把某个节点提上来成为这个节点所在树的根。那怎么破?首先,Access操作可以把某个节点到根节点的路径打通,splay可以把某个节点拧到其所在Auxiliary Tree的根上。然而,注意Auxiliary Tree和原树并不是同一个概念,怎么办?

考虑Auxiliary Tree中维护的东西,左子树代表父亲,右子树代表儿子。而且Access之后,这个节点一定位于一颗Auxiliary Tree中的尾部。那么考虑把它提到根,显然它不会有右子树,而左子树中都是它的父亲,我们只要把它的父亲变成儿子就可以了。那么答案很显然,翻转操作。

下面是代码

void makeroot(int p) {Access(p); Splay(p); t[p].rev ^= 1;}

Find

判断连通性的时候,我们要知道一个节点在原树上的根。这就很简单,Access一下,然后Splay一下,显然当前结点所在Auxiliary Tree中包含根。那么只要找到深度最小的即可,所以一直往左走就好了。

int find(int p) {    Access(p); Splay(p);    while(t[p].ch[0]) p = t[p].ch[0];    return p;}

到目前为止,难点操作已经结束,其实理解这个算法就是理解access 和 make root,其他的就很简单了。

云霄飞车——Link和Cut

Link

把两个节点连接起来。做法就是吧儿子make root,然后直接把儿子连到父亲即可

void Link(int u, int v) {    makeroot(u);    t[u].f = v;}

Cut

把两个节点断开。做法就是把父亲make root,儿子Access之后splay一下,那么显然这个时候父亲在splay中的父亲是儿子。而儿子在splay中的第一个左儿子是父亲。直接断开即可(置零)

void Cut(int u, int v) {    makeroot(u); Access(v); Splay(v);    t[u].f = t[v].ch[0] = 0;}

到此为止,所有的关于lct的操作已经结束,怎么样?lct不是很难吧,和树剖一样一样的。

终了

剩下的操作因题目而异,回到这道题,还需要一个change来单点修改,query来路径询问,操作的套路其实都一样,不再特殊写了,直接贴代码咯。

关于lct复杂度的证明,可以参见《QTREE解法的一些研究》,链接的话戳这里,里面的关于lct的讲解其实也很清楚,图也画得比我好看。

呼呼,lct终于学完了,可以继续刷题咯,又是新的一轮被虐,啦啦啦!

#include<iostream>#include<cstdlib>#include<cstdio>#include<cstring>#include<algorithm>#include<map>#include<cmath>#define maxn 330000#define ls t[p].ch[0]#define rs t[p].ch[1]#define pa t[p].fusing namespace std;int read() {    char ch = getchar(); int x = 0, f = 1;    while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}    while(ch >= '0' && ch <= '9') {x = x * 10 - '0' + ch; ch = getchar();}    return x * f;} struct node {    int f, ch[2];    int rev, v, s;}t[maxn]; int n, m, q[maxn], top;bool wh(int p) {return t[pa].ch[1] == p;}bool Isroot(int p) {return t[pa].ch[0] != p && t[pa].ch[1] != p;}  void push_down(int p) {    if(t[p].rev) {        t[ls].rev ^= 1; t[rs].rev ^= 1;        swap(ls, rs); t[p].rev = 0;    }}void push_up(int p) {    top = 0; q[++top] = p;    for(int i = p; !Isroot(i); i = t[i].f)         q[++top] = t[i].f;    for(int i = top; i; --i)         push_down(q[i]);}void update(int p) {t[p].s = t[ls].s ^ t[rs].s ^ t[p].v;} void Rotate(int p) {    int f = pa, g = t[f].f, c = wh(p);    if(!Isroot(f)) t[g].ch[wh(f)] = p; t[p].f = g;    t[f].ch[c] = t[p].ch[c ^ 1]; if(t[f].ch[c]) t[t[f].ch[c]].f = f;    t[p].ch[c ^ 1] = f; t[f].f = p;    update(f);} void Splay(int p) {    push_up(p);    for(; !Isroot(p); Rotate(p))         if(!Isroot(t[p].f)) Rotate(wh(p) == wh(pa) ? pa : p);    update(p);} void Access(int p) {    for(int pre = 0; p; pre = p, p = pa) {        Splay(p);        t[p].ch[1] = pre;        update(p);    }} void makeroot(int p) {Access(p); Splay(p); t[p].rev ^= 1;}int find(int p) {    Access(p); Splay(p);    while(t[p].ch[0]) p = t[p].ch[0];    return p;} void Cut(int u, int v) {    makeroot(u); Access(v); Splay(v);    t[u].f = t[v].ch[0] = 0;} void Link(int u, int v) {    makeroot(u);    t[u].f = v;} void Change(int x, int y) {Access(x); Splay(x); t[x].v = y; update(x);}void Query(int x, int y) {makeroot(x); Access(y); Splay(y); printf("%d\n", t[y].s);} int main(){    n = read(); m = read();    for(int i = 1;i <= n; ++i) t[i].v = t[i].s = read();     for(int i = 1;i <= m; ++i) {        int opt = read(), x = read(), y = read();        if(opt == 0) Query(x, y);        if(opt == 1 && find(x) != find(y)) Link(x, y);        if(opt == 2 && find(x) == find(y)) Cut(x, y);        if(opt == 3) Change(x, y);    }    return 0;}