Study notes - LCT

Posted by mcfmullen on Wed, 26 Jan 2022 02:37:50 +0100

preface

It's too difficult ~ but the winter camp talks about this thing. Open the pit in advance. Pre cheese

Define

#define ls tr[x].ch[0]
#define rs tr[x].ch[1]
struct Tree{int ch[2],val,fa,rev,xv;}tr[MAXN];

LCT?

How does LCT surpass the general tree section and balance tree to achieve the effect of maintaining a forest?

LCT has some properties after real chain subdivision and Splay auxiliary tree:

  1. The sequence traversal of the tree maintained in each Splay is a chain in the original tree, and the depth is increasing. Therefore, the precursors and successors in Splay are the father and son in the original tree.
  2. Each node is included and contained in only one Splay.
  3. Each Splay is connected by a virtual edge. The so-called virtual edge actually means that the son can find the father one way, but the father can't find the son, because the father only recognizes the only son passing through the real edge.

Note that all the following operations are for the original tree, and the auxiliary tree is only used for auxiliary.

Access

The only basic operation of LCT is access. access(x) means to turn all the edges on the path between the point \ (x \) and the currently specified root node into real edges, which is equivalent to putting \ (x \) and the root node into the same Splay, so as to open up the path between them. And \ (x \) is the last node in the real chain.

The process starts from the Splay where \ (x \) is currently located. Each time, rotate \ (x \) to the root of the Splay, and then turn the edge of its father into reality (actually let the father recognize the son), and then perform the same processing on the current father of \ (x \) until it reaches the root node currently specified in the original tree. I'm too lazy to put the picture. Look FlashHu blog Go.

void access(int x){for(int s=0;x;s=x,x=tr[x].fa)splay(x),rs=s,pushup(x);}

However, it should be noted that after the above operations, the resulting Splay is not balanced, so splay(x) is generally added after access(x).

Makeroot

As the name suggests, make \ (x \) the root, that is, specify \ (x \) as the root of the current original tree. We consider the first property of LCT. If you want to \ (x \) the root of the real original tree, in the auxiliary tree, \ (x \) must traverse the first one in the Splay where the root is located. So we think of an access(x) to \ (x \), so that \ (x \) becomes the last of the middle order traversal (because the middle order traversal reflects the depth increasing sequence of a real chain on the original tree. After access(x), \ (x \) is naturally the node with the deepest depth of the real chain where the root is located). In order to make it the first one, we flip the Splay where it is located, so that the middle order traversal becomes the first one, and \ (x \) becomes the node with the smallest depth in the original tree (that is, the root).

void pushr(int x){swap(ls,rs);tr[x].rev^=1;}//Can everyone know the balance tree of literature and art
void makeroot(int x){access(x);splay(x);pushr(x);}

Findroot

That is, find the root of the original tree where \ (x \) is located (LCT is to maintain the forest). Through thinking about Makeroot, it is easy to think that we want to put \ (x \) in the same Splay as the root. Then the root is actually the first in the sequence traversal in Splay, so just keep looking for a son. For a search, the complexity is usually guaranteed by play (x).

int findroot(int x){access(x);splay(x);while(ls) pushdown(x),x=ls;splay(x);return x;}

Split

Used to access a chain in the LCT. We can do a lot with makelot. First make \ (x \) become the root, then connect the path from \ (Y \) to \ (x \), and then connect \ (Y \) through play (y). At this time, \ (Y \) is the root of this Splay, and the information of this chain exists in \ (Y \).

void split(int x,int y){makeroot(x);access(y);splay(y);}

Link

Connect an edge from \ (x \) to \ (Y \). First make \ (x \) the root, and then if \ (x,y \) is not in the same connected block, make the father of \ (x \) become \ (Y \).

void link(int x,int y){makeroot(x);if(findroot(y)!=x) tr[x].fa=y;}

Cut

Break the edge between \ (x,y \). This is complicated. If there is no edge between \ (x,y \), or there are multiple nodes between \ (x,y \), it is illegal.

The first is makeroot(x).

So we have some judgment to remove these situations:

  1. If \ (x,y \) is not in the same connected block;
  2. If the father of \ (y \) is not \ (x \), there are other points between them;
  3. If \ (Y \) has a left son in Splay, it means that there are some points between \ (x,y \) in the middle order traversal.

These are illegal.

Then leave the right son of \ (x \) and the father of \ (y \) empty, indicating that this side is disconnected.

Where you might be confused

Why is \ (Y \) the right son of \ (x \)? Because we have makeroot(x). At this time, if \ (Y \) is directly connected with \ (x \), then \ (Y \) must be one of the biological sons of \ (x \). When findroot(y), we have access(y), that is \ (Y \) and \ (x \) in a Splay. Then if \ (x,y \) is directly connected, \ (Y \) can only be the right son of \ (x \).

void cut(int x,int y){
	makeroot(x);
	if(findroot(y)!=x||tr[y].fa!=x||tr[y].ch[0]) return;
	tr[y].fa=tr[x].ch[1]=0;pushup(x);
}

Isroot

Note that this is not a function applied to the original tree. It indicates whether \ (x \) is the root node of the Splay where \ (x \) is located. The judgment is actually very simple. If it is a root, its father does not recognize it.

Why do you need this?

Here are the differences between the Splay in LCT and the general Splay. One of them is this thing. Since there are many splays, we can start from the root \ (U \) of a Splay through tr [u] FA to get a node in another Splay. This is terrible, which means that if you can't judge whether \ (U \) is a root, the whole tree will be destroyed. So there was this thing.

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

About Splay

Everyone can write Splay. But the Splay here is a little different, but the overall situation is the same.

Let's start with Splay's code.

void pushup(int x){tr[x].xv=tr[ls].xv^tr[rs].xv^tr[x].val;}
void pushdown(int x){
	if(!tr[x].rev) return;
	if(ls)pushr(ls);if(rs)pushr(rs);tr[x].rev=0;
}
void rot(int x){
	int f=tr[x].fa,k=(x==tr[f].ch[1]),g=tr[f].fa,v=tr[x].ch[k^1];
	if(!isroot(f)) tr[g].ch[f==tr[g].ch[1]]=x;tr[x].ch[k^1]=f;tr[f].ch[k]=v;//diff
	if(v) tr[v].fa=f;tr[f].fa=x;tr[x].fa=g;
	pushup(f);
}
void splay(int x){
	stack<int> st;int tmp=x;st.push(tmp);
	while(!isroot(tmp)) tmp=tr[tmp].fa,st.push(tmp);
	while(!st.empty()) pushdown(st.top()),st.pop();//diff
	while(!isroot(x)){//diff
		int f=tr[x].fa,g=tr[f].fa;
		if(!isroot(f))
			rot((f==tr[g].ch[1])^(x==tr[f].ch[1])?x:f);
		rot(x);
	}pushup(x);
}

There are a lot of things here, but push up and push down don't care about it. I added / / diff to the above differences. Next, let me explain one by one:

  1. When spinning alone, because we can't let the father of Splay's root recognize the son connected by the virtual edge, we should pay attention to whether the father is the root when we are in rot;
  2. It is found that there is an extra lump in front of the \ (play \) operator. This thing is very simple. It's a downlink mark. After the \ (play \) operation, the parent-child relationship of the current tree changes, so you can only double spin after paying off the "debt" before that;
  3. The end criterion of double rotation is whether to go to the root of the current Splay.

Other precautions

  1. With regard to pushup, it should be noted that when any parent-child relationship changes, we should think about whether \ (Pushup \);
  2. When changing, you must pay attention to whether it will affect other nodes. Most of the time, you need to change after \ (play \);
  3. To be continued......

Examples

P3690 [template] Link Cut Tree
The first LCT in life... Basic operation, just set the board.

$\texttt{Code}$
#include<bits/stdc++.h>
#define ll long long
#define inf (1<<30)
#define INF (1ll<<60)
#define pb push_back
using namespace std;
const int MAXN=1e5+10;
struct LCT{
	#define ls tr[x].ch[0]
	#define rs tr[x].ch[1]
	struct Tree{int ch[2],val,fa,rev,xv;}tr[MAXN];
	void pushr(int x){swap(ls,rs);tr[x].rev^=1;}
	bool isroot(int x){return tr[tr[x].fa].ch[0]!=x&&tr[tr[x].fa].ch[1]!=x;}
	void pushup(int x){tr[x].xv=tr[ls].xv^tr[rs].xv^tr[x].val;}
	void pushdown(int x){
		if(!tr[x].rev) return;
		if(ls)pushr(ls);if(rs)pushr(rs);tr[x].rev=0;
	}
	void rot(int x){
		int f=tr[x].fa,k=(x==tr[f].ch[1]),g=tr[f].fa,v=tr[x].ch[k^1];
		if(!isroot(f)) tr[g].ch[f==tr[g].ch[1]]=x;tr[x].ch[k^1]=f;tr[f].ch[k]=v;
		if(v) tr[v].fa=f;tr[f].fa=x;tr[x].fa=g;
		pushup(f);
	}
	void splay(int x){
		stack<int> st;int tmp=x;st.push(tmp);
		while(!isroot(tmp))	tmp=tr[tmp].fa,st.push(tmp);
		while(!st.empty()) pushdown(st.top()),st.pop();
		while(!isroot(x)){
			int f=tr[x].fa,g=tr[f].fa;
			if(!isroot(f))
				rot((f==tr[g].ch[1])^(x==tr[f].ch[1])?x:f);
			rot(x);
		}pushup(x);
	}
	void access(int x){for(int s=0;x;s=x,x=tr[x].fa)splay(x),rs=s,pushup(x);}
	void makeroot(int x){access(x);splay(x);pushr(x);}
	int findroot(int x){access(x);splay(x);while(ls) pushdown(x),x=ls;splay(x);return x;}
	void split(int x,int y){makeroot(x);access(y);splay(y);}
	void link(int x,int y){makeroot(x);if(findroot(y)!=x) tr[x].fa=y;}
	void cut(int x,int y){
		makeroot(x);
		if(findroot(y)!=x||tr[y].fa!=x||tr[y].ch[0]) return;
		tr[y].fa=tr[x].ch[1]=0;pushup(x);
	}
}T;
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&T.tr[i].val);
	int op,x,y;
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&op,&x,&y);
		if(op==0) T.split(x,y),printf("%d\n",T.tr[y].xv);
		else if(op==1) T.link(x,y);
		else if(op==2) T.cut(x,y);
		else T.splay(x),T.tr[x].val=y;
	}
}

Topics: Algorithm