您的位置:首页 > 其它

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

2017-09-09 15:50 531 查看

算法学习之:动态树(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不一定是子树节点个数最多的,因为这棵树的结构会随时变化,下文会提到。

数据结构登场——平衡树

由上面
4000
的操作,我们得到了若干条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的人就知道,可以把Access翻译成“废嫡立庶”。(没错这是我瞎说的)

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>
using namespace std;
const int N = 330000;
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;
}
int s
, val
, ch
[2], rev
, st
, fa
, top;
bool wh(int p) {return ch[fa[p]][1] == p;}
bool Isroot(int p) {return ch[fa[p]][wh(p)] != p;}
void Update(int p) {s[p] = val[p] ^ s[ch[p][0]] ^ s[ch[p][1]];}
void Pushdown(int p) {
if(rev[p]) {
rev[p] ^= 1; swap(ch[p][0], ch[p][1]);
rev[ch[p][1]] ^= 1; rev[ch[p][0]] ^= 1;
}
}
void Pushup(int p) {
top = 0; st[++top] = p;
for(int i = p; !Isroot(i); i = fa[i]) st[++top] = fa[i];
for(int i = top; i; --i) Pushdown(st[i]);
}

void Rotate(int p) {
int f = fa[p], g = fa[f], c = wh(p);
if(!Isroot(f)) ch[g][wh(f)] = p; fa[p] = g;
ch[f][c] = ch[p][c ^ 1]; if(ch[f][c]) fa[ch[f][c]] = f;
ch[p][c ^ 1] = f; fa[f] = p;
Update(f);
}
void Splay(int p) {
Pushup(p);
for(; !Isroot(p); Rotate(p))
if(!Isroot(fa[p])) Rotate(wh(fa[p]) == wh(p) ? fa[p] : p);
Update(p);
}
void Access(int p) {
for(int pre = 0; p; pre = p, p = fa[p]) {
Splay(p);
ch[p][1] = pre;
Update(p);
}
}
void Makeroot(int p) {Access(p); Splay(p); rev[p] ^= 1;}
int Find(int p) {for(Access(p), Splay(p); ch[p][0]; p = ch[p][0]) ; return p;}
void Cut(int u, int v) {Makeroot(u); Access(v); Splay(v); ch[v][0] = fa[u] = 0;}
void Link(int u, int v) {Makeroot(u); fa[u] = v;}
void Change(int u, int v) {Access(u); Splay(u); val[u] = v; Update(u);}
void Query(int u, int v) {Makeroot(u); Access(v); Splay(v); printf("%d\n", s[v]);}

int main() {
int n = read(), m = read();
for(int i = 1;i <= n; ++i) val[i] = s[i] = read();
while(m--) {
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;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: