Link-Cut Tree

前言

update 2021.2.16 加上了目录,修复了部分 (markdown)

大佬博客

这是一篇质量还行的博客

前排放大佬的博客用来(orz)

FlashHu orz

前置芝士

Splay

据说( equire{enclose}enclose{horizontalstrike}{ ext{Treap}})也可以,但蒟蒻作者不会

树链剖分(如果你学过的话可以更好理解,没学过也没有关系)

定义

又到了喜闻乐见的百度百科自学时间

真·定义:

动态树,( ext{Link-Cut Tree}),简称( ext{LCT})

其中的( ext{Link})指加边,( ext{Cut})指删边

类似于树链剖分,但是线段树变成了(Splay)(大概是这样的)

我们就可以在树上自由地删边、连边了,当然也可以搞其它的

作用

  • 查询、修改链上的信息(最值,总和等)

  • 换根

  • 动态连边、删边

  • 合并两棵树、分离一棵树

  • 动态维护连通性

  • 敬请期待更多操作

讲解

令人窒息的百度百科(反正我再也不会看了)

如果你一头雾水地回来了,那么我又双叒叕成功了

( ext{LCT})是由很多( ext{Splay})维护的一个数据结构

其中最重要的当然就是( equire{enclose}enclose{horizontalstrike}{ ext{Splay}})

零、大概的实现

如果有一棵树,我们将每个点选一个儿子为偏爱儿子(类似于树链剖分的重儿子,但是这是我们自己定的)

然后偏爱儿子与父亲的边为实边,其余的为虚边

每一个实边相连的部分为一棵( ext{Splay})

我们将每一个实边相连的部分(一条链)叫做偏爱路径((Preferred Path))

组成的( ext{Splay})叫做辅助树((Auxiliary Tree))

学专业术语就是出去装A_C用的

假设我们可以构造这么一棵树,长这样:(红色为实边,绿色为虚边)

其中框出来的部分就是一棵( ext{Splay})

图好丑啊啊啊啊啊!!!

注意不要把( ext{F})给忘了

对于每一棵( ext{Splay}),我们按深度的从小到大排序

排序完了之后当然就是:前面(左边)的是祖先,后面(右边)的是后代

( ext{LCT})不画了,(lazy),但是( ext{LCT})要满足上面的规则

其中:

虚边认父不认子(如只记录(F)的父亲是(E),但不记录(E)的儿子是(F))

实边都要认(两边都要存)

这些东西是为了保证我们接下来的操作

一、( ext{access})

核心操作!!!

如果你英语好,或者知道这个东西

你就可以知道这个单词的意思是访问

具体的,它用来实现把节点(x)与树根连接起来(搞到一棵( ext{Splay})里)

跟访问有什么关系呢?

我们举个栗子,把上面的树搬下来:

假设我们要( ext{access(L)}),就要把(A)(L)路径上的所有边全部变为实边

怎么实现呢?

1.首先我么要把(L)转为自己所在的( ext{Splay})的根

注意我画的图不是按照排序规律画的(因为懒)

现在(K)(L)的左边

2.然后,由于我们马上就要把(L)(E)连成实边了,所以我们要把(L)的右儿子赋为空(认父不认子),尽管它并没有右儿子

3.接着,我们更新一下(L)的信息(由下到上)

4.最后,我们走向(L)的父亲(E)(通过虚边走向父亲)

现在我们在(E)

(E)转到根

把抢了位置的(G)替换为上来的(L)(更新右儿子)

更新(E)的信息(由下到上)

走向(E)的父亲(A),注意我们已经换到了根,所以父亲为(A)(通过虚边走向父亲)

(A)也进行这样的操作,那么直到再像父亲走的时候走到了空节点,结束

总结,其实循环只有四步:

1.转到根

2.改变右儿子

3.更新信息

4.走向父亲

void access(int x)//访问 
{
	for(int y = 0; x ;x = t[y = x].f)
    {
		splay(x);
		t[x].ch[1] = y;
		up(x);
	}
}

二、( ext{makeroot})(换根)

懂了第一个操作,这个操作就显得简单很多

连通(x)到根,把(x)转到根

其它点的深度都改变了,但是我们发现他们的深度刚好都翻转了过来

所以我们可以这样:

void pushr(int x)//翻转操作
{
	swap(t[x].ch[0],t[x].ch[1]);
	t[x].r ^= 1;
}
void makeroot(int x)//换根
{
	access(x);
	splay(x);
	pushr(x);
} 

三、( ext{findroot})(找根)

就是找当前点所在的( ext{Splay})的根?

Pi!

是原树的根

首先把(x)到根连通,把(x)转到根,一直向左走,最小的即为根

int findroot(int x)//找根(原树中) ,就是最小的那个 
{
	access(x);
	splay(x);
	while(t[x].ch[0]) down(x),x = t[x].ch[0];
	splay(x);
	return x;
}

四、( ext{merge})(合并)

自己给这个操作取的名字

我们询问(x,y)的路径上的信息,就要把(x,y)合到一棵树里面

故取名(merge),由于(merge)是关键字,所以简写为(merg) (好丑)

我们将(x)置为树的根(( ext{makeroot})),通过( ext{access})操作连通(x,y),将(y)旋转到根,路径上的点就在(y)的子树中,我们可以通过访问(y)中的信息,获取路径上的信息

void merg(int x,int y)//提取路径,把x和y搞到一棵Splay里面,再乱搞 
{
	makeroot(x);
	access(y);
	splay(y);
}

五、( ext{link})(加边)

判断是否已经连通

如果没连通,连虚边

判连通的两个方法:

1.普遍的

(x)置为根,找(y)所在的树的根,如果不是(x),不连通,否则连通

2.特殊的:只有加边

并查集维护

void link(int x,int y)//连边 
{
	makeroot(x);
	if(findroot(y) != x) t[x].f = y;
}

六、( ext{cut})(删边)

首先判断是否连通

连通之后再判断是否有一条相连的边

我们可以通过这样的方法判断:

(x)置为根

如果(y)的父亲是(x)并且(y)没有左子树(即(y)的祖先就(x)一个)

删掉:(y)的父亲置为(0)(x)的右儿子置为(0)

别忘了更新(x)的信息!!!

void cut(int x,int y)//删边 
{
	makeroot(x);
	if(findroot(y) == x && t[y].f == x && !t[y].ch[0])//保证它们相连 
	{
		t[y].f = t[x].ch[1] = 0;
		up(x);
	}
}

练习

板题(洛谷)

[COCI 2009] OTOCI / 极地旅行社(洛谷)

弹飞绵羊(洛谷)

洞穴勘测(洛谷)

其实都是板题,就弹飞绵羊稍微有一点思维

代码

板题代码

//12252024832524
#include <cstdio>
#include <algorithm>
using namespace std; 

typedef long long LL;
const int MAXN = 100005;
int n,m;
int v[MAXN];

int Read()
{
	int x = 0,f = 1;char c = getchar();
	while(c > '9' || c < '0'){if(c == '-')f = -1;c = getchar();}
	while(c >= '0' && c <= '9'){x = (x*10) + (c^48);c = getchar();}
	return x * f;
}
void Put1(int x)
{
	if(x > 9) Put1(x/10);
	putchar(x%10^48);
}
void Put(int x)
{
	if(x < 0) putchar('-'),x = -x;
	Put1(x);
}
template <typename T>T Max(T x,T y){return x > y ? x : y;}
template <typename T>T Min(T x,T y){return x < y ? x : y;}
template <typename T>T Abs(T x){return x < 0 ? -x : x;}

struct Splay//LinkCutTree
{
	int tot;
	int st[MAXN];//栈 
	struct node
	{
		int val,ch[2],f;
		bool r;//翻转标记 
	}t[MAXN];
	
	bool nroot(int x)//判断节点是否为一个Splay的根 
	{
		return t[t[x].f].ch[0] == x || t[t[x].f].ch[1] == x;
		//如果连的是轻边,他的父亲的儿子里就没有它 
	}
	void pushr(int x)//翻转操作
	{
		swap(t[x].ch[0],t[x].ch[1]);
		t[x].r ^= 1;
	}
	void up(int x)
	{
		t[x].val = t[t[x].ch[0]].val ^ t[t[x].ch[1]].val ^ v[x];
	}
	void down(int x)
	{
		if(t[x].r) 
		{
			if(t[x].ch[0]) pushr(t[x].ch[0]);
			if(t[x].ch[1]) pushr(t[x].ch[1]);
			t[x].r = 0;
		}
	}
	bool chk(int x)
	{
		return (t[t[x].f].ch[1] == x);
	}
	void rotate(int x)
	{
		int y = t[x].f;
		int z = t[y].f;
		int k = chk(x);
		int w = t[x].ch[k^1];
		
		t[y].ch[k] = w;
		if(w) t[w].f = y;
		
		if(nroot(y)) t[z].ch[chk(y)] = x;
		t[x].f = z;
		
		t[x].ch[k^1] = y;
		t[y].f = x;
		
		up(y);
		up(x);
	}
	void splay(int x,int to = 0)
	{
		int y = x,z = 0;
		st[++z] = y;
		while(nroot(y)) st[++z] = y = t[y].f;
		while(z) down(st[z--]);
		while(nroot(x))
		{
			y = t[x].f;
			if(nroot(y))
			{
				if(chk(x) != chk(y)) rotate(x);
				else rotate(y);
			}
			rotate(x);
		}
		up(x);
	}
	void access(int x)//访问 
	{
		for(int y = 0; x ;x = t[y = x].f)
        {
			splay(x);
			t[x].ch[1] = y;
			up(x);
		}
	}
	void makeroot(int x)//换根
	{
		access(x);
		splay(x);
		pushr(x);
	} 
	int findroot(int x)//找根(原树中) ,就是最小的那个 
	{
		access(x);
		splay(x);
		while(t[x].ch[0]) down(x),x = t[x].ch[0];
		splay(x) ;
		return x;
	}
	void merg(int x,int y)//提取路径,把x和y搞到一棵Splay里面,再乱搞 
	{
		makeroot(x);
		access(y);
		splay(y);
	}
	void link(int x,int y)//连边 
	{
		makeroot(x);
		if(findroot(y) != x) t[x].f = y;
	}
	void cut(int x,int y)//删边 
	{
		makeroot(x);
		if(findroot(y) == x && t[y].f == x && !t[y].ch[0])//保证它们相连 
		{
			t[y].f = t[x].ch[1] = 0;
			up(x);
		}
	}
}lct;

int main()
{
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n = Read();
	m = Read();
	for(int i = 1;i <= n;++ i) v[i] = Read();
	while(m --)
	{
		int opt = Read(),x = Read(),y = Read();
		if(opt == 0) {lct.merg(x,y);Put(lct.t[y].val);putchar('
');}
		else if(opt == 1) {lct.link(x,y);}
		else if(opt == 2) {lct.cut(x,y);}
		else if(opt == 3) {lct.splay(x,0);v[x] = y;}
		else printf("fuck ccf
");
	}
	return 0;
}

OTOCI代码 (貌似不开O2过不了,令人窒息的常数)

//12252024832524
#include <cstdio>
#include <algorithm>
using namespace std; 

typedef long long LL;
const int MAXN = 30005;
int n,m;
int v[MAXN];
int st[MAXN];//栈 
int F[MAXN];
char opt[10];

inline int Read()
{
	int x = 0,f = 1;char c = getchar();
	while(c > '9' || c < '0'){if(c == '-')f = -1;c = getchar();}
	while(c >= '0' && c <= '9'){x = (x*10) + (c^48);c = getchar();}
	return x * f;
}
inline void Put1(int x)
{
	if(x > 9) Put1(x/10);
	putchar(x%10^48);
}
inline void Put(int x)
{
	if(x < 0) putchar('-'),x = -x;
	Put1(x);
}
template <typename T>T Max(T x,T y){return x > y ? x : y;}
template <typename T>T Min(T x,T y){return x < y ? x : y;}
template <typename T>T Abs(T x){return x < 0 ? -x : x;}

char gc()
{
	char c = getchar();
	while(c > 'z' || c < 'a') c = getchar();
	return c;
}
int findSet(int x)
{
	if(F[x] != x) F[x] = findSet(F[x]);
	return F[x];
}
struct Splay//LinkCutTree
{
	int tot;
	struct node
	{
		int val,ch[2],f;
		bool r;//翻转标记 
	}t[MAXN];
	
	inline bool nroot(int x)//判断节点是否为一个Splay的根 
	{
		return t[t[x].f].ch[0] == x || t[t[x].f].ch[1] == x;
		//如果连的是轻边,他的父亲的儿子里就没有它 
	}
	inline void pushr(int x)//翻转操作
	{
		swap(t[x].ch[0],t[x].ch[1]);
		t[x].r ^= 1;
	}
	inline void up(int x)
	{
		t[x].val = t[t[x].ch[0]].val + t[t[x].ch[1]].val + v[x];
	}
	inline void down(int x)
	{
		if(t[x].r) 
		{
			if(t[x].ch[0]) pushr(t[x].ch[0]);
			if(t[x].ch[1]) pushr(t[x].ch[1]);
			t[x].r = 0;
		}
	}
	inline bool chk(int x)
	{
		return (t[t[x].f].ch[1] == x);
	}
	inline void rotate(int x)
	{
		int y = t[x].f;
		int z = t[y].f;
		int k = chk(x);
		int w = t[x].ch[k^1];
		
		t[y].ch[k] = w;
		if(w) t[w].f = y;
		
		if(nroot(y)) t[z].ch[chk(y)] = x;
		t[x].f = z;
		
		t[x].ch[k^1] = y;
		t[y].f = x;
		
		up(y);
		up(x);
	}
	inline void splay(int x,int to = 0)
	{
		int y = x,z = 0;
		st[++z] = y;
		while(nroot(y)) st[++z] = y = t[y].f;
		while(z) down(st[z--]);
		while(nroot(x))
		{
			y = t[x].f;
			if(nroot(y))
			{
				if(chk(x) != chk(y)) rotate(x);
				else rotate(y);
			}
			rotate(x);
		}
		up(x);
	}
	inline void access(int x)//访问 
	{
		for(int y = 0; x ;x = t[y = x].f)
        {
			splay(x);
			t[x].ch[1] = y;
			up(x);
		}
	}
	inline void makeroot(int x)//换根
	{
		access(x);
		splay(x);
		pushr(x);
	} 
	inline int findroot(int x)//找根(原树中) ,就是最小的那个 
	{
		access(x);
		splay(x);
		while(t[x].ch[0]) down(x),x = t[x].ch[0];
		splay(x) ;
		return x;
	}
	inline int dis(int x,int y)//询问路径,把x和y搞到一棵Splay里面,再乱搞 
	{
		makeroot(x);
		access(y);
		splay(y);
		return t[y].val;
	}
	bool qconnect(int x,int y)
	{
		if(findSet(x) == findSet(y)) return 1;
		return 0;
	}
	inline void link(int x,int y)//连边 
	{
		makeroot(x);
		if(!qconnect(x,y)) t[x].f = y,printf("yes
"),F[findSet(x)] = findSet(y);
		else printf("no
");
	}
	inline void cut(int x,int y)//删边 
	{
		makeroot(x);
		if(qconnect(x,y) && t[y].f == x && !t[y].ch[0])//保证它们相连 
		{
			t[y].f = t[x].ch[1] = 0;
			up(x);
		}
	}
}lct;

int main()
{
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n = Read();
	for(register int i = 1;i <= n;++ i) v[i] = Read(),F[i] = i;
	int x,y;
	for(register int Q = Read(); Q ;-- Q)
	{
		char opt = gc();
		x = Read();
		y = Read();
		if(opt == 'e') {if(!lct.qconnect(x,y)){printf("impossible
");continue;}Put(lct.dis(x,y));putchar('
');}
		else if(opt == 'b') {lct.link(x,y);}
		else if(opt == 'p') {lct.splay(x,0);v[x] = y;}
	}
	return 0;
}

弹飞绵羊代码

//12252024832524
#include <cstdio>
#include <algorithm>
using namespace std; 

typedef long long LL;
const int MAXN = 200005;
int n,m;
int v[MAXN],tox[MAXN];
int st[MAXN];//栈 

inline int Read()
{
	int x = 0,f = 1;char c = getchar();
	while(c > '9' || c < '0'){if(c == '-')f = -1;c = getchar();}
	while(c >= '0' && c <= '9'){x = (x*10) + (c^48);c = getchar();}
	return x * f;
}
inline void Put1(int x)
{
	if(x > 9) Put1(x/10);
	putchar(x%10^48);
}
inline void Put(int x)
{
	if(x < 0) putchar('-'),x = -x;
	Put1(x);
}
template <typename T>T Max(T x,T y){return x > y ? x : y;}
template <typename T>T Min(T x,T y){return x < y ? x : y;}
template <typename T>T Abs(T x){return x < 0 ? -x : x;}

char gc()
{
	char c = getchar();
	while(c > 'z' || c < 'a') c = getchar();
	return c;
}
struct Splay//LinkCutTree
{
	int tot;
	struct node
	{
		int val,ch[2],f;
		bool r;//翻转标记 
	}t[MAXN];
	
	inline bool nroot(int x)//判断节点是否为一个Splay的根 
	{
		return t[t[x].f].ch[0] == x || t[t[x].f].ch[1] == x;
		//如果连的是轻边,他的父亲的儿子里就没有它 
	}
	inline void pushr(int x)//翻转操作
	{
		swap(t[x].ch[0],t[x].ch[1]);
		t[x].r ^= 1;
	}
	inline void up(int x)
	{
		t[x].val = t[t[x].ch[0]].val + t[t[x].ch[1]].val + v[x];
	}
	inline void down(int x)
	{
		if(t[x].r) 
		{
			if(t[x].ch[0]) pushr(t[x].ch[0]);
			if(t[x].ch[1]) pushr(t[x].ch[1]);
			t[x].r = 0;
		}
	}
	inline bool chk(int x)
	{
		return (t[t[x].f].ch[1] == x);
	}
	inline void rotate(int x)
	{
		int y = t[x].f;
		int z = t[y].f;
		int k = chk(x);
		int w = t[x].ch[k^1];
		
		t[y].ch[k] = w;
		if(w) t[w].f = y;
		
		if(nroot(y)) t[z].ch[chk(y)] = x;
		t[x].f = z;
		
		t[x].ch[k^1] = y;
		t[y].f = x;
		
		up(y);
		up(x);
	}
	inline void splay(int x,int to = 0)
	{
		int y = x,z = 0;
		st[++z] = y;
		while(nroot(y)) st[++z] = y = t[y].f;
		while(z) down(st[z--]);
		while(nroot(x))
		{
			y = t[x].f;
			if(nroot(y))
			{
				if(chk(x) != chk(y)) rotate(x);
				else rotate(y);
			}
			rotate(x);
		}
		up(x);
	}
	inline void access(int x)//访问 
	{
		for(int y = 0; x ;x = t[y = x].f)
        {
			splay(x);
			t[x].ch[1] = y;
			up(x);
		}
	}
	inline void makeroot(int x)//换根
	{
		access(x);
		splay(x);
		pushr(x);
	} 
	inline int findroot(int x)//找根(原树中) ,就是最小的那个 
	{
		access(x);
		splay(x);
		while(t[x].ch[0]) down(x),x = t[x].ch[0];
		splay(x) ;
		return x;
	}
	inline int dis(int x,int y)//询问路径,把x和y搞到一棵Splay里面,再乱搞 
	{
		makeroot(x);
		access(y);
		splay(y);
		return t[y].val;
	}
	void link(int x,int y)//连边 
	{
		makeroot(x);
		if(findroot(y) != x) t[x].f = y;
	}
	inline void cut(int x,int y)//删边 
	{
		makeroot(x);
		if(findroot(y) == x && t[y].f == x && !t[y].ch[0])//保证它们相连 
		{
			t[y].f = t[x].ch[1] = 0;
			up(x);
		}
	}
}lct;

int main()
{
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n = Read();
	for(register int i = 1;i <= n;++ i) 
	{
		v[i] = 1;
		tox[i] = Read();
		if(i + tox[i] > n)
			tox[i] = n+1-i;
		lct.link(i,i+tox[i]);
	}
	for(register int Q = Read(); Q ;-- Q)
	{
		int opt = Read(); 
		if(opt == 1) Put(lct.dis(Read()+1,n+1)),putchar('
');
		else if(opt == 2)
		{
			int x = Read()+1;
			lct.cut(x,x+tox[x]);
			tox[x] = Read();
			if(x + tox[x] > n) tox[x] = n+1-x;
			lct.link(x,x+tox[x]);
		}
		else printf("fuck ccf
");
	}
	return 0;
}

洞穴勘测

//12252024832524
#include <cstdio>
#include <algorithm>
using namespace std; 

typedef long long LL;
const int MAXN = 10005;
int n,m;
int v[MAXN],tox[MAXN];
int st[MAXN];//栈 

inline int Read()
{
	int x = 0,f = 1;char c = getchar();
	while(c > '9' || c < '0'){if(c == '-')f = -1;c = getchar();}
	while(c >= '0' && c <= '9'){x = (x*10) + (c^48);c = getchar();}
	return x * f;
}
inline void Put1(int x)
{
	if(x > 9) Put1(x/10);
	putchar(x%10^48);
}
inline void Put(int x)
{
	if(x < 0) putchar('-'),x = -x;
	Put1(x);
}
template <typename T>T Max(T x,T y){return x > y ? x : y;}
template <typename T>T Min(T x,T y){return x < y ? x : y;}
template <typename T>T Abs(T x){return x < 0 ? -x : x;}

char gc()
{
	char c = getchar();
	while(c > 'Z' || c < 'A') c = getchar();
	return c;
}
struct Splay//LinkCutTree
{
	int tot;
	struct node
	{
		int val,ch[2],f;
		bool r;//翻转标记 
	}t[MAXN];
	
	inline bool nroot(int x)//判断节点是否为一个Splay的根 
	{
		return t[t[x].f].ch[0] == x || t[t[x].f].ch[1] == x;
		//如果连的是轻边,他的父亲的儿子里就没有它 
	}
	inline void pushr(int x)//翻转操作
	{
		swap(t[x].ch[0],t[x].ch[1]);
		t[x].r ^= 1;
	}
	inline void up(int x)
	{
		t[x].val = t[t[x].ch[0]].val + t[t[x].ch[1]].val + v[x];
	}
	inline void down(int x)
	{
		if(t[x].r) 
		{
			if(t[x].ch[0]) pushr(t[x].ch[0]);
			if(t[x].ch[1]) pushr(t[x].ch[1]);
			t[x].r = 0;
		}
	}
	inline bool chk(int x)
	{
		return (t[t[x].f].ch[1] == x);
	}
	inline void rotate(int x)
	{
		int y = t[x].f;
		int z = t[y].f;
		int k = chk(x);
		int w = t[x].ch[k^1];
		
		t[y].ch[k] = w;
		if(w) t[w].f = y;
		
		if(nroot(y)) t[z].ch[chk(y)] = x;
		t[x].f = z;
		
		t[x].ch[k^1] = y;
		t[y].f = x;
		
		up(y);
		up(x);
	}
	inline void splay(int x,int to = 0)
	{
		int y = x,z = 0;
		st[++z] = y;
		while(nroot(y)) st[++z] = y = t[y].f;
		while(z) down(st[z--]);
		while(nroot(x))
		{
			y = t[x].f;
			if(nroot(y))
			{
				if(chk(x) != chk(y)) rotate(x);
				else rotate(y);
			}
			rotate(x);
		}
		up(x);
	}
	inline void access(int x)//访问 
	{
		for(int y = 0; x ;x = t[y = x].f)
        {
			splay(x);
			t[x].ch[1] = y;
			up(x);
		}
	}
	inline void makeroot(int x)//换根
	{
		access(x);
		splay(x);
		pushr(x);
	} 
	inline int findroot(int x)//找根(原树中) ,就是最小的那个 
	{
		access(x);
		splay(x);
		while(t[x].ch[0]) down(x),x = t[x].ch[0];
		splay(x) ;
		return x;
	}
	inline int dis(int x,int y)//询问路径,把x和y搞到一棵Splay里面,再乱搞 
	{
		makeroot(x);
		access(y);
		splay(y);
		return t[y].val;
	}
	bool qconnect(int x,int y)
	{
		makeroot(x);
		if(findroot(y) == x) return 1;
		return 0;
	}
	void link(int x,int y)//连边 
	{
		if(!qconnect(x,y)) t[x].f = y;
	}
	inline void cut(int x,int y)//删边 
	{
		if(qconnect(x,y) && t[y].f == x && !t[y].ch[0])//保证它们相连 
		{
			t[y].f = t[x].ch[1] = 0;
			up(x);
		}
	}
}lct;

int main()
{
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n = Read();
	for(register int Q = Read(); Q ;-- Q)
	{
		char c = gc();
		if(c == 'C') lct.link(Read(),Read());
		else if(c == 'D') lct.cut(Read(),Read());
		else 
		{
			if(lct.qconnect(Read(),Read())) printf("Yes
");
			else printf("No
");
		}
	}
	return 0;
}
原文地址:https://www.cnblogs.com/PPLPPL/p/13395292.html