• 【学习笔记】Link Cut Tree


    Link Cut Tree(LCT) 是一种用来解决动态树问题的数据结构。

    前置芝士:Splay

    部分参考:FlashHu 的博客OI Wiki - Link Cut Tree

    一、实链剖分和 LCT

    实链剖分:将原树的每条边分成实边虚边。实边中,儿子认父亲,父亲也认儿子;虚边中,儿子认父亲,但父亲不认儿子。实边相连得到实链。每条实链可用数据结构维护。每条边的实虚是可变的,因此我们要采用更为高级的数据结构——Splay

    LCT:用 Splay 来维护动态的树链剖分,每条实链都用一个 Splay 维护。

    LCT 可以干这些事:

    1. 在两点之间连一条边
    2. 删去两点之间的边
    3. 修改某个点/两点间路径的权值
    4. 查询某个点的权值/两点间路径上的权值和/异或和/……
    5. 指定某点为原树的根
    6. 判断两点之间是否连通
    7. ……

    单次操作时间复杂度为 (operatorname{O}(log n))

    二、性质

    主要有三点,后面许多操作都基于这些性质(下文出现的“深度”均指节点在原树中的深度)。

    1. 每一个 Splay 维护的是一条从上往下、深度严格递增的路径;
    2. 每个节点包含且仅包含于一个 Splay中;
    3. 实边包含在 Splay 中,而虚边是由一棵 Splay 的指向该 Splay 中序遍历最靠前的节点(最左边的节点)在原树中的父亲(在原树中这条链的顶端的父亲)。

    三、实现

    1. 数组含义(以 P3690 为例)

    int ch[N][2];//两个儿子
    int val[N];//该节点的权值
    int xs[N];//子树权值异或和
    int fa[N];//父亲
    int tag[N];//翻转标记
    int stk[N];//数组模拟栈
    

    2. Splay 基本操作

    这些都是 Splay 基本操作。其中 splay 与平常我们写的有些不同,需要注意。

    还有一个 notrt 函数,作用是:判断一个点 p 是否不是 p 所在 splay 的根,只需要判断是否和父亲实边相连(p 的父亲是否认 p)

    inline int ident(int p) {
    	return ch[fa[p]][1] == p;
    }
    
    inline void update(int p) {
    	xs[p] = xs[ch[p][0]] ^ xs[ch[p][1]] ^ val[p];
    }
    
    inline bool notrt(int p) {
    	return ch[fa[p]][0] == p || ch[fa[p]][1] == p;
    }
    
    inline void connect(int p, int f, int cc) {//父子相认,是实边
    	fa[p] = f;
    	ch[f][cc] = p;
    }
    
    inline void flip(int p) {//翻转函数,打上标记并交换这个节点的左右儿子
    	if(!p) return;
    	tag[p] ^= 1;//0^1=1,1^1=0 即若原来无则现在有,若原来有则现在无(区间翻转的性质)
    	swap(ch[p][0], ch[p][1]);
    }
    
    inline void push(int p) {//下传标记,清空该节点的标记并传给两个儿子
    	if(!tag[p]) return;
    	tag[p] = 0;
    	flip(ch[p][0]);
    	flip(ch[p][1]);
    }
    inline void rotate(int p) {
    	int q = fa[p], r = fa[q], cp = ident(p), cq = ident(q), w = ch[p][cp ^ 1];
    	fa[p] = r;
    	if(notrt(q)) ch[r][cq] = p;//特别要注意,若notrt不为真说明q-r是一条虚边,旋转之后应该还是虚边,不能认儿子
    	connect(w, q, cp);
    	connect(q, p, cp ^ 1);
    	update(q);
    }
    
    inline void splay(int p) {
    	int top, q;
    	for(stk[top = 1] = q = p; notrt(q);) stk[++top] = q = fa[q];
    	while(top) push(stk[top--]);//从上至下把根到这个点的路径上所有点的标记下传
    	for(; notrt(p); rotate(p))//这里要判notrt,保证在当前Splay内
    		if(notrt(q = fa[p])) rotate(ident(q) == ident(p) ? q : p);
    	update(p);
    }
    

    3. LCT 基本操作

    • ( ext{access(p)})

    作用:把 p 到根这条路径上的边全部变为实边,构成一棵Splay。

    由于性质 1,该路径上其它链都要给这条链让路,也就是把路径上的每个点到该路径以外的点之间的实边变成虚边。

    由于性质 3,虚边一定连结着一棵 Splay 的根。所以要先把 p 转到该 Splay 的根。

    假设在转之前,p 和 q 之间有虚边相连(q 是一棵深度比 p 大的Splay 的根),说明 q 的父亲是 p,那么直接 ch[p][1]=q 即可。儿子变了,要更新节点信息。

    然后 q=p,p 则向上爬,变为它的父亲,进入下一个 Splay。然后重复上述操作。

    简而言之:

    1. 旋转到根
    2. 换右儿子
    3. 更新信息
    4. 向上爪巴

    下面来模拟一下这个过程(由于笔者又菜又懒,直接用了 FlashHu 的文章中的图片):

    ( exttt{access(N)})

    inline void access(int p) {
    	for(int q = 0; p; p = fa[p]) {
    		splay(p);
    		ch[p][1] = q;
    		update(q = p);//右儿子改变,需要更新
    	}
    }
    
    • ( ext{make_root(p)})

    作用:把 p 变成原树中的根。

    想让p成为原树中的根,就要让它与现在的根之间的路径全为实边。

    access(p) 后,p 一定是 p 所在 Splay 中深度最大的点。因为 access 中第一遍循环 q=0ch[p][1]=qch[p][1]=0。也就是说右子树为空,根据性质 1 可知该 Splay 中无比 p 深度更大的节点。

    根是树中深度最小的节点,但它是深度最大的,所以要将 p 所在的 Splay 整个翻转(使用类似线段树的懒标记)。

    inline void make_root(int p) {
    	access(p);
    	splay(p);
    	flip(p);
    }
    
    • ( ext{find_root(p)})

    作用:寻找x所在原树的树根,常用于判断连通性。

    首先 access(p),让 p 和 root 处于同一 Splay。

    由于根节点深度最小,它一定在 Splay 的最左边(中序遍历中最前面的节点)。把 p 旋到根,一直向左走直到没有左儿子。这个节点就是根节点。最后 splay(p) 保证复杂度正确。

    inline int find_root(int p) {
    	access(p);
    	splay(p);
    	for(; ch[p][0]; p = ch[p][0]) push(p);
    	splay(p);
    	return p;
    }
    
    • ( ext{split(p,q)})

    作用:将路径 p-q 上的所有边变为实边,成为一个Splay。

    之前已经实现过 access,可以将原树的根到某节点的路径上的所有边变为实边,成为一个Splay。make_root(p),p就变成了原树的根节点。将路径 p-q 上的所有边变为实边就转化为将原树的根到q的路径上的所有边变为实边。

    inline void split(int p, int q) {
    	make_root(p);
    	access(q);
    	splay(q);//此时splay的根为q,整个链的信息可以直接从q获取
    }
    
    • ( ext{link(p,q)})

    作用:在 pq 之间连一条边。

    原树的根节点是没有(虚边相连的)父亲的,那么让 p 成为原树根,父亲指向 q 就连接了 p 和 q。

    inline void link(int p, int q) {
    	make_root(p);
    	if(find_root(p) ^ find_root(q)) fa[p] = q;//fdrt(p)=fdrt(q)说明两点联通,再连边不合法
    	//此时这条边是虚边,不在同一splay,不能update
    }
    
    • ( ext{cut(p,q)})

    作用:断开连接 pq 的边。

    首先判断 p,q 是否有边。 make_root(p),p 成为原树的根,深度最小。所以 q 若与 p 相连,q 一定在 p 的右子树内。由于性质 1,这棵 Splay 中序遍历中 p 和 q 一定要相邻才行。那就有三种情况:

    1. p,q不连通
    2. p,q连通,但q的父亲不是p
    3. q的左子树不为空
    inline void cut(int p, int q) {
    	make_root(p);
    	if(find_root(q) ^ p || fa[q] ^ p || ch[q][0]) return;
    	fa[q] = ch[p][1] = 0;
    	update(p);
    }
    

    四、其他神奇的操作

    咕咕咕

    五、例题

    模板题,所有操作前文已讲,直接上代码。

    #include <cstdio>
    #include <cstring>
    
    using namespace std;
    
    #define in inline
    typedef long long ll;
    in int max(int x, int y) {return x > y ? x : y;}
    in int min(int x, int y) {return x < y ? x : y;}
    in void swap(int &x, int &y) {x ^= y ^= x ^= y;}
    #define rei register int
    #define rep(i, l, r) for(rei i = l, i##end = r; i <= i##end; ++i)
    #define repd(i, r, l) for(rei i = r, i##end = l; i >= i##end; --i)
    char inputbuf[1 << 23], *p1 = inputbuf, *p2 = inputbuf;
    #define getchar() (p1 == p2 && (p2 = (p1 = inputbuf) + fread(inputbuf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
    in int read() {
    	int res = 0; char ch = getchar(); bool f = true;
    	for(; ch < '0' || ch > '9'; ch = getchar())
    		if(ch == '-') f = false;
    	for(; ch >= '0' && ch <= '9'; ch = getchar())
    		res = res * 10 + (ch ^ 48);
    	return f ? res : -res;
    }
    const int N = 1e5 + 15;
    
    int ch[N][2], val[N], xs[N], fa[N], tag[N], stk[N], tot;
    
    in int ident(int p) {
    	return ch[fa[p]][1] == p;
    }
    
    in void update(int p) {
    	xs[p] = xs[ch[p][0]] ^ xs[ch[p][1]] ^ val[p];
    }
    
    in void connect(int p, int f, int cc) {
    	fa[p] = f;
    	ch[f][cc] = p;
    }
    
    in void flip(int p) {
    	if(!p) return;
    	tag[p] ^= 1;
    	swap(ch[p][0], ch[p][1]);
    }
    
    in void push(int p) {
    	if(!tag[p]) return;
    	tag[p] = 0;
    	flip(ch[p][0]);
    	flip(ch[p][1]);
    }
    
    in bool notrt(int p) {
    	return ch[fa[p]][0] == p || ch[fa[p]][1] == p;
    }
    
    in void rotate(int p) {
    	int q = fa[p], r = fa[q], cp = ident(p), cq = ident(q), w = ch[p][cp ^ 1];
    	fa[p] = r;
    	if(notrt(q)) ch[r][cq] = p;
    	connect(w, q, cp);
    	connect(q, p, cp ^ 1);
    	update(q);
    }
    
    in void splay(int p) {
    	int top, q;
    	for(stk[top = 1] = q = p; notrt(q);) stk[++top] = q = fa[q];
    	while(top) push(stk[top--]);
    	for(; notrt(p); rotate(p))
    		if(notrt(q = fa[p])) rotate(ident(q) == ident(p) ? q : p);
    	update(p);
    }
    
    in void access(int p) {
    	for(int q = 0; p; p = fa[p]) {
    		splay(p);
    		ch[p][1] = q;
    		update(q = p);
    	}
    }
    
    in void mkrt(int p) {
    	access(p);
    	splay(p);
    	flip(p);
    }
    
    in int fdrt(int p) {
    	access(p);
    	splay(p);
    	for(; ch[p][0]; p = ch[p][0]) push(p);
    	splay(p);
    	return p;
    }
    
    in void split(int p, int q) {
    	mkrt(p);
    	access(q);
    	splay(q);
    }
    
    in void link(int p, int q) {
    	mkrt(p);
    	if(fdrt(p) ^ fdrt(q)) fa[p] = q;
    }
    
    in void cut(int p, int q) {
    	mkrt(p);
    	if(fdrt(q) ^ p || fa[q] ^ p || ch[q][0]) return;
    	fa[q] = ch[p][1] = 0;
    	update(p);
    }
    
    signed main() {
    	int n = read(), q = read(), opt, x, y;
    	rep(i, 1, n) val[i] = read();
    	for(; q; --q) {
    		opt = read(); x = read(); y = read();
    		switch(opt) {
    			case 0 : split(x, y); printf("%d
    ", xs[y]); break;
    			case 1 : link(x, y); break;
    			case 2 : cut(x, y); break;
    			case 3 : splay(x); val[x] = y; break;
    		}
    	}
    	return 0;
    }
    

    咕咕咕

  • 相关阅读:
    时间序列的小波分析
    粒子群算法优化BP生物能神经网络
    day:3.9基础复习
    计算机网络通信基础
    面向对象的补充
    python中的类和对象
    R语言基础
    函数(2)
    python开发第四篇:函数(1)
    Python开发【第三篇】:Python基本数据类型
  • 原文地址:https://www.cnblogs.com/creating-2007/p/13330087.html
Copyright © 2020-2023  润新知