算法学习之:动态树(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;}
- 算法学习之:动态树(link-cut-tree)及bzoj3282Tree例题详解
- Link-Cut-Tree 动态树算法
- 学习笔记-动态树Link-Cut-Tree
- 动态树(Link-Cut Tree)学习小结
- LCT(Link Cut Tree)动态树学习笔记
- 浅谈Link-Cut-Tree([林可砍树]LCT动态树)附例题 Hdu4010
- Link Cut Tree(动态树)
- 动态树 Link Cut Tree
- (新知)动态树--LCT(Link-Cut-Tree)--入门教程
- 专题总结:动态树 LCT(Link cut tree)
- bzoj2002Bounce 弹飞绵羊 动态树(Link-Cut-Tree)
- luoguP3690 【模板】Link Cut Tree (动态树)
- 【动态树初探】link-cut tree
- 【动态树初探】link-cut tree
- 【HDU】4010 Query on The Trees 动态树之Link Cut Tree(LCT)
- LCT(Link-Cut-Tree)学习
- BZOJ 2631 tree 动态树(Link-Cut-Tree)
- BZOJ 3282 Tree Link-Cut-Tree 动态树
- 王爽汇编语言实验10.3
- 数据结构--链表
- 窗口绘制的知识汇总
- librdkafak消费者 最小c语言版本
- socket通信服务端编程
- 算法学习之:动态树(link-cut-tree)及bzoj3282Tree例题详解
- CSS3笔记——转换
- redis配置后端启动——支持集群部署
- 欢迎使用CSDN-markdown编辑器
- XTU C语言程序设计实践作业2
- C#基础-056 字符串练习题
- return的用法是什么?若用在for循环中,还会执行下一次循环吗?
- JZOJ5354. 【NOIP2017提高A组模拟9.9】导弹拦截
- Spring学习之Spring 配置(四)