算法学习之:动态树(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; }
相关文章推荐
- 浅谈Link-Cut-Tree([林可砍树]LCT动态树)附例题 Hdu4010
- NOI级别的超强数据结构——Link-cut-tree(动态树)学习小记(update guadually...)
- 【动态树之link_cut_tree学习小记】
- 【Link Cut Tree】动态树的理解(入门)
- Link-cut-tree 学习记录 & hdu4010
- 学习一个LinkCutTree
- Link Cut Tree(动态树)
- 动态树 Link Cut Tree
- 【算法之动态规划(四)】动态规划笔试例题详解
- LuoguP3690 【模板】Link Cut Tree (动态树) LCT模板
- Link Cut Tree(LCT )学习笔记
- 转【算法之动态规划(四)】动态规划笔试例题详解
- LCT(Link-Cut-Tree)学习
- 动态树(Link-Cut-Tree)结构与实现简讲
- [Luogu 3690]【模板】Link Cut Tree (动态树)
- BZOJ 2049 Sdoi2008 Cave 洞穴勘测 动态树 Link-Cut-Tree
- Link Cut Tree学习小记
- LCT (Link-Cut-Tree) 学习笔记
- bzoj3282 Tree&luogu3690 【模板】Link Cut Tree (动态树)
- P3690 【模板】Link Cut Tree (动态树)