Link-Cut Tree(存档文,不发布)


Link-Cut Tree 是一种用来维护动态森林连通性的数据结构,适用于动态树问题。它采用类似树链剖分的轻重边路径剖分,把树边分为实边和虚边,并用 Splay 来维护每一条实路径。Link-Cut Tree 的基本操作复杂度为均摊 ,但常数因子较大,一般效率会低于树链剖分。

定义

对于原森林,引入如下定义:

  • 偏爱儿子(Preferred Child),一个节点最多有一个偏爱儿子;
  • 偏爱边(Preferred Edge),偏爱儿子与其父亲相连的边。
  • 偏爱路径(Preferred Path),由偏爱边连接而成的链;

原森林中偏爱路径由非偏爱边连接,构成 Link-Cut Tree。我们称 Link-Cut Tree 上的非偏爱边为虚边。同样的,对于以外的边称之为实边。

实边和虚边都是有向的,由子节点指向父节点。首尾相连的实边组成的不可延伸的链叫做路径。路径中深度最大的节点称为路径头部,深度最小的节点称为路径尾部。

对于 Link-Cut Tree 上的一条路径,用 Splay 树来维护信息,以节点的深度为关键字排序。Splay 树中一个节点的左子树所有节点比当前节点在森林中的深度小,右子树的所有节点比当前节点在森林中的深度大。不难发现,同一条路径上的点深度连续且互不相同。

实现时只需维护 Link-Cut Tree。其中每一棵 Splay 树的形态影响 Link-Cut Tree 的形态,但由于 Splay​ 树上的节点按照深度有序排列着(不改变相对次序),故对用 Link-Cut Tree 处理原森林节点关系没有影响。

在记录 Link-Cut Tree 时,用 fa 存储父亲的信息。如果当前点是其所在 Splay 树的根节点,其 fa 指向所在的路径的路经尾部在原森林中的父亲,否则指向其在 Splay 树的父亲节点。但是每个节点的儿子信息则完全是 Splay 树中的儿子信息,即轻边的儿子不会在父节点有记录。因此我们只需更改每个节点的右儿子即可,并不需要重置右儿子的父亲信息。

struct Node {
	int fa, ch[2];
	int sum, max, value;
	bool reversed; 
} Tr[MAXN];

操作

Link-Cut Tree 支持以下几种基本操作:

  1. Access(u),“访问”某个节点 u u u,被“访问”过的节点会与根节点之间以偏爱路径相连,并且该节点为路径头部(最下端);
  2. MakeRoot(u),将某个节点 u u u 置为其所在树的根节点;
  3. FindRoot(u),查找某个节点 u u u 所在树的根节点;
  4. Link(u, v),将两个节点 u u u v v v 连接,执行操作后 u u u 成为 v v v 的父节点;
  5. Cut(u, v),将两个节点 u u u v v v 分离,执行操作后 v v v 及其子节点组成单独的一棵树;

操作表面上是对原森林处理,本质上是在 Link-Cut Tree 上做文章。

Access​ 操作

Access 操作是动态树问题最重要的操作,其目的是在同一棵 Splay 树上进行查询,修改,删除操作,且保证这棵树上只有需要查询的一条链上的点。

Access(u)​ 将根节点到点 u u u 的路径变为偏爱路径,将点 u u u 与其儿子的连边均变为非偏爱边。

对于当前点到根路径上的每一个点,不外乎进行如下操作:

  1. 将当前点 u u u 与儿子的连边均变为非偏爱边。

    将点 u u u 旋转至当前 Splay 树的根,令其右儿子为 null \textrm{null} null

  2. 设点 u u u 位于路径上的儿子为 v v v,使点 u , v u,v u,v 的连边成为偏爱边。

    在操作 1 的基础上,将点 u u u 的右儿子指向点 v v v 所在的 Splay 树。此时 u → f a u\rightarrow fa ufa 是轻边。

void access(int u) {
	for(int v = 0; u; v = u, u = Tr[u].fa)
		splay(u), Tr[u].ch[1] = v, pushup(u);
}

MakeRoot 操作

MakeRoot(u) 将​ u u u 置为其所在树的根节点。该操作等价于把点 u u u 到根节点所经过的所有边方向取反。

首先执行 Access(u),将该节点与根节点之间用一条完整的路径连接,然后翻转这条路径即可。

void makeRoot(int u) {
	access(u), splay(u), Tr[u].reversed ^= 1;
}

FindRoot 操作

FindRoot(u) 查找某个节点 u u u 所在树的根节点,主要用以判断连通性。

首先执行 Access(u),将该节点与根节点之间用一条完整的路径连接。由于根节点深度最小,直接在 Splay 树上寻找。

int findRoot(int u) {
	access(u), splay(u);
	pushdown(u); while(Tr[u].ch[0]) pushdown(u = Tr[u].ch[0]);
	return u;
}

Link 操作

Link(u, v) 将两个节点 u u u v v v 连接,执行操作后 u u u 成为 v v v 的父节点。

首先执行 MakeRoot(v),然后将 v v v 的父亲指向 u u u,相当于连一条轻边。

void link(int u, int v) {
	makeRoot(v); 
	if (findRoot(u) != v) Tr[v].fa = u; //判断连边的合法性
}

Cut 操作

Cut(u, v) 将两个节点 u u u v v v 分离,执行操作后 v v v 及其子节点组成单独的一棵树。

依次进行如下操作:

  1. 将点 u u u 置为其所在树的根节点,以保证 v v v u u u 的子节点;
  2. v v v 执行 Access 操作,将 v v v u u u 之间用一条完整的路径连接;
  3. v v v 执行 Splay 操作,将 v v v 置于其所在 Splay 树的根节点;
  4. 在 Splay 树上将 v v v 与其左子树分离,即将路径断开。
void cut(int u, int v){
	makeRoot(u), access(v), splay(v);
	if (tr[v].ch[0] != u || tr[tr[v].ch[0]].ch[1]) return; //为保证合法,要求二者深度连续。
	Tr[u].fa = Tr[v].ch[0] = 0, pushup(v);
}

实现

IsRoot 函数

IsRoot(u) 函数判断点 u u u 是否为一棵 Splay 树的根节点。如果是,则返回真值。

根据对 fa 的定义,不难得到判断方法。

bool isRoot(int x) {
	return Tr[Tr[x].fa].ch[0] != x && Tr[Tr[x].fa].ch[1] != x;
}

Splay 操作

bool get(int x) {
	return Tr[Tr[x].fa].ch[1] == x; 
}

void rotate(int x) {
	int y = Tr[x].fa, z = Tr[y].fa, k = get(x);
	if(!isRoot(y)) Tr[z].ch[get(y)] = x; Tr[x].fa = z; 
	Tr[y].ch[k] = Tr[x].ch[k ^ 1], Tr[Tr[x].ch[k ^ 1]].fa = y; 
	Tr[x].ch[k ^ 1] = y, Tr[y].fa = x;
	pushUp(y), pushUp(x);
} 

void splay(int x) {
	update(x);
	for(int fa = Tr[x].fa; !isRoot(x); rotate(x), fa = Tr[x].fa) {
		if(!isRoot(fa)) rotate(get(fa) == get(x) ? fa : x);
	}
}

其余辅助函数

void reverse(int x) {
	swap(Tr[x].ch[0], Tr[x].ch[1]), Tr[x].reversed ^= 1;
}

void pushUp(int x) {
	Tr[x].sum = Tr[Tr[x].ch[0]].sum ^ Tr[Tr[x].ch[1]].sum ^ Tr[x].value; 
}

void pushDown(int x) {
    if (!Tr[x].reversed) return;
    reverse(Tr[x].ch[0]), reverse(Tr[x].ch[1]);
	Tr[x].reversed = 0;
}

void update(int x) {
	if (!isRoot(x)) update(Tr[x].fa);
	pushDown(x);
}

完整代码

#include <bits/stdc++.h>

using namespace std;

const int MAXN = 300000 + 10;

struct Node {
	int fa, ch[2];
	int sum, value;
	bool reversed;
} Tr[MAXN];

int n, m, x, y, type;

bool isRoot(int x) {
	return Tr[Tr[x].fa].ch[0] != x && Tr[Tr[x].fa].ch[1] != x;
}

void reverse(int x) {
	swap(Tr[x].ch[0], Tr[x].ch[1]), Tr[x].reversed ^= 1;
}

void pushUp(int x) {
	Tr[x].sum = Tr[Tr[x].ch[0]].sum ^ Tr[Tr[x].ch[1]].sum ^ Tr[x].value; 
}

void pushDown(int x) {
    if (!Tr[x].reversed) return;
    reverse(Tr[x].ch[0]), reverse(Tr[x].ch[1]);
	Tr[x].reversed = 0;
}

void update(int x) {
	if (!isRoot(x)) update(Tr[x].fa);
	pushDown(x);
}

bool get(int x) { 
	return Tr[Tr[x].fa].ch[1] == x; 
}

void rotate(int x) {
	int y = Tr[x].fa, z = Tr[y].fa, k = get(x);
	if(!isRoot(y)) Tr[z].ch[get(y)] = x; Tr[x].fa = z; 
	Tr[y].ch[k] = Tr[x].ch[k ^ 1], Tr[Tr[x].ch[k ^ 1]].fa = y; 
	Tr[x].ch[k ^ 1] = y, Tr[y].fa = x;
	pushUp(y), pushUp(x);
} 

void splay(int x) {
	update(x);
	for(int fa = Tr[x].fa; !isRoot(x); rotate(x), fa = Tr[x].fa) {
		if(!isRoot(fa)) rotate(get(fa) == get(x) ? fa : x);
	}
}

void access(int u) {
	for (int v = 0; u; v = u, u = Tr[u].fa) 
		splay(u), Tr[u].ch[1] = v, pushUp(u);
}

void makeRoot(int u) {
	access(u), splay(u), reverse(u);
}

int findRoot(int u) {
	access(u), splay(u);
	pushDown(u); while (Tr[u].ch[0]) pushDown(u = Tr[u].ch[0]);
	return u;
}

void link(int u, int v) {
	makeRoot(v); if (findRoot(u) != v) Tr[v].fa = u;
}

void cut(int u, int v) {
	makeRoot(u), access(v), splay(v);
	if (Tr[v].ch[0] != u || Tr[Tr[v].ch[0]].ch[1]) return;
	Tr[u].fa = Tr[v].ch[0] = 0, pushUp(v);
}
	
int main() {
	scanf("%d %d", &n, &m);
	
	for (int i = 1; i <= n; i++) {
		scanf("%d", &x);
		Tr[i].value = Tr[i].sum = x;
	}
	
	while (m--) {
		scanf("%d %d %d", &type, &x, &y);
		switch (type) {
        	case 0 : makeRoot(x), access(y), splay(y), printf("%d\n", Tr[y].sum); break;
        	case 1 : link(x, y); break;
        	case 2 : cut(x, y); break;
        	case 3 : splay(x), Tr[x].value = y, pushUp(x); break;
		}
	}
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值