Link Cut Tree

Luogu P3690 动态树(LCT)

给定 \(n\) 个点、每个点的权值和 \(m\) 次操作:

  • 0 x y 代表询问从 \(x\)\(y\) 的路径上的点的权值的 \(\operatorname{xor}\) 和。保证 \(x\)\(y\) 是联通的。
  • 1 x y 代表连接 \(x\)\(y\),若 \(x\)\(y\) 已经联通则无需连接。
  • 2 x y 代表删除边 \((x,y)\),不保证边 \((x,y)\) 存在。
  • 3 x y 代表将点 \(x\) 上的权值变成 \(y\)

\(1\leq n\leq 10^5,1\leq m\leq3\times10^5\)


本文中维护信息,以维护路径异或和为例。

动态树问题

考虑树链剖分可以维护树上路径信息和子树信息,但是树的形态是静态的。如果树的形态会发生改变,那么树链剖分就必须重构。

当树的形态发生改变时,这种问题,就称之为动态树问题

事实上,动态树问题处理的是若干棵树构成的森林。

Link Cut Tree

其实 LCT 是一个比较暴力的结构,复杂度纯粹源于势能均摊。


下文的 Splay 节点都基于:

struct node{
	int value;
	int sum,father,child[2];
	bool reverse;
}t[N+1];

实链剖分

仍然考虑将树剖为链来处理,但是重链剖分和长链剖分都不太适合。因为树的形态会发生改变,子树大小、子树高度都会随着树的形态改变而改变。

因此我们引入一种新的剖分方式——实链剖分。

实链剖分就是随便剖。——xxy

对于一个点,我们自行指定一个子节点为实儿子连实边,其余子节点均为虚儿子,连虚边。

对于实边组成的链,称之为实链,用一棵 Splay 树维护一条实链的链区间。

正因为实链剖分是随便剖,因此实边、虚边对于原本需要维护的信息没有什么影响,可以随便改。

辅助树

对于一棵原树,将其实链剖分后可以建立其辅助树。例如:

004

其辅助树为:

005

每条原树上的实链都对应辅助树上的一棵 Splay。辅助树上同样分实边、虚边:

  • Splay 内部的边为实边,对应原树上的实边。
  • Splay 外部的边为虚边,只从 Splay 的根节点指向原树上实边顶部节点的父节点,虚边是单向的。

LCT 通过这样的辅助树来维护实链信息,进而维护原树的信息,因为辅助树可以维护原树的所有信息,且辅助树上更容易实现实边、虚边的转化。

\(\operatorname{child}_0(x),\operatorname{child}_1(x)\) 分别为 \(x\) 在辅助树上的左、右子节点,\(\operatorname{father}(x)\)\(x\) 在辅助树上的父节点。

Splay 函数操作

显然,维护了若干棵 Splay。此部分针对的节点信息都是 Splay 上的信息,而不是辅助树上的,也不是原树上的。

check

判断是左子节点还是右子节点。

bool check(int p){
	return t[t[p].father].child[1]==p;
}

isRoot

用于判断是否为 Splay 的根,根据虚边父节点不认子节点判断即可。

bool isRoot(int p){
	return t[t[p].father].child[0]!=p&&t[t[p].father].child[1]!=p;
}

up

维护 Splay 子树信息。

void up(int p){
	t[p].sum=t[t[p].child[0]].sum^t[p].value^t[t[p].child[1]].sum;
}

down

下传标记。

LCT 一般都需要额外维护区间翻转标记。

void down(int p){
	if(t[p].reverse){
		t[t[p].child[0]].reverse^=1;
		t[t[p].child[1]].reverse^=1;
		swap(t[p].child[0],t[p].child[1]);
		t[p].reverse=false;
	}
}

update

如果没有下放标记,你把 down 写在 rotate 里面是错的,因为标记要从上往下下放,不然你下放了又有新的。因此在 LCT 的 splay 操作前,需要调用 update 来将根节点至当前节点路径上所有的标记都下放。

(在 Splay 维护区间操作中,因为你找到对应节点的时候已经顺便下放了标记,所以没有 update。)

递归寻找即可。

void update(int p){
	if(!isRoot(p)){
		update(t[p].father);
	}
	down(p);
}

rotate

同 Splay,唯一细节是需要通过 isRoot(y) 来判断 \(y\) 是否为根,从而判断是否应当更新 \(y\) 的父节点 \(z\) 的子节点信息。

void rotate(int x){
    int y=t[x].father,z=t[y].father;
    bool mode=check(x);
    t[y].child[mode]=t[x].child[!mode];
    t[x].child[!mode]=y;
    if(!isRoot(y)){
        t[z].child[check(y)]=x;
    }
    if(t[y].child[mode]){
        t[t[y].child[mode]].father=y;
    }
    t[y].father=x; 
    t[x].father=z;
    up(y);
    up(x);
}

splay

splay 之前 update 一下即可。

void splay(int x){
    update(x);
    while(!isRoot(x)){
        int p=t[x].father;
        if(isRoot(p)){
            rotate(x);
            break;
        }
        if(check(p)==check(x)){
            rotate(p);
            rotate(x);
        }else{
            rotate(x);
            rotate(x);
        }
    }
}

LCT 函数操作

access

LCT 的核心操作之一。

原树的根\(\textit{root}\)access(x) 用于将 \(\textit{root}\sim x\) 路径上的所有点放入一棵 Splay,即将 \(x\sim\textit{root}\) 这条路径变成实链,并将其他原来与这条路径相连的实边都变成虚边。

同样以上文的树为例:

004

其辅助树为:

005

access(N) 为例,则我们希望原树变为:

006

最终,我们得到的实链为:\(A\rightarrow C\rightarrow G\rightarrow H\rightarrow I\rightarrow L\rightarrow N\)

考虑 Splay 维护的是原树上的实链,中序遍历是从上至下。 \(N\) 和实链上下一个节点连了实边,要换成虚边;但是整个右子树都可以丢掉,因此直接断开 \(N\)\(\operatorname{child}_1(N)=O\)(此部分接下来的图均为辅助树):

007

之后我们希望把 \(L\rightarrow N\) 这一段实链接到 \(N\) 的父节点 \(I\) 的实链上,从而得到 \(I\rightarrow L\rightarrow N\) 的一条实链。

先将 \(I\) splay 到根,之后 \(I\) 所在 Splay 的左子树即向上实链的一部分,右子树直接丢掉,换为 \(N\)

008

循环往复:

009

010

最终,access 的操作策略即:

  1. 对于 \(\operatorname{access}(x)\),初始维护 \(p=0\)
  2. \(x\) splay 到根;
  3. \(\operatorname{child}_1(x)\leftarrow p\)
  4. 更新 \(x\) 的子树信息,即调用 up
  5. \(p\leftarrow x,x\leftarrow\operatorname{father}(x)\)
void access(int x){
    for(int p=0;x;p=x,x=t[x].father){
        splay(x);
        t[x].child[1]=p;
        up(x);
    }
}

makeRoot

我们现在已经可以通过 access 实现 \(\textit{root}\sim x\) 的路径信息维护,但是往往我们需要维护的路径 \(u\sim v\) 并不是根节点到另一个节点的路径。我们考虑强行给原树换根,这样就可以通过 access 操作之后很好用 Splay 树维护。

\(x\) 进行 makeRoot 的流程:

  1. 先对其进行 access 操作,得到 \(\textit{root}\rightarrow x\) 的一条实链;

  2. 之后把 \(x\) splay 到 Splay 树的根,等价于将其换为了原树的根节点;

  3. 但是这样使得原来 \(\textit{root}\rightarrow x\) 的实链的父子关系「上下颠倒」,因此需要对这棵 Splay 进行整体区间翻转操作。打上一个翻转标记即可。

    其实颠倒对于辅助树是没有什么影响的,因为辅助树的实边是无向的;但是,原树的边是有向的,强行换根会导致父子关系颠倒,所以需要区间翻转。

    (如果对于区间翻转还看不懂,可以看到下文 find 再想一想。)

void makeRoot(int x){
    access(x);
    splay(x);
    t[x].reverse^=1;
    swap(t[x].child[0],t[x].child[1]);
}

find

find 用于查找节点 \(x\) 所在原树的根节点 \(\textit{root}\)

access 得到 \(\textit{root}\rightarrow x\) 的实链,之后把 \(x\) splay 到根节点,再一直走左子节点求最小即可。

需要 splay 操作来保证复杂度。

int find(int x){
    access(x);
    splay(x);
    down(x);
    while(t[x].child[0]){
        x=t[x].child[0];
        down(x);
    }
    splay(x);
    return x;
}
为什么 find 不需要打翻转标记

考虑 find 并没有改变原树根——尽管都执行了 accesssplay

事实上,LCT 也没有真正维护过原树根,这是一个比较抽象的概念。

makeRoot 需要打翻转标记,是因为强行换根为 $x$ 之后,实际上 $x$ 还有祖先节点——即其左子树,这时,应当将其放入右子树内,成为其子节点。

split

split 用于提取任意两点 \(u,v\) 之间的路径成为一棵 Splay。

\(u\) makeRoot,之后再对 \(v\) 进行 access 操作即可。

需要对 \(v\) 进行 splay 操作来保证复杂度。特别地,在此之后 \(u\sim v\) 路径信息便对应以 \(v\) 为根的 Splay,信息都在 \(v\) 上面。

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

考虑任意连边 \((u,v)\) 不好做,先对 \(u\) makeRoot,使其成为原树根。之后考虑 \(v\) 是否在 \(u\) 子树内,通过 find 操作即可判断。

否则,\(u\) 已经没有父节点,直接把 \(u\) 挂在 \(v\) 子树上,连虚边即可。这也就是实链剖分的好处——随便剖。

void link(int u,int v){
    makeRoot(u);
    if(find(v)!=u){
        t[u].father=v; 
    }
}

cut

同理,先对 \(u\) makeRoot,之后就是判断边 \((u,v)\) 存在。

由于我们维护的是辅助树,因此判断边有些麻烦,充要条件为:\(\operatorname{find}(v)=u\land\operatorname{father}(v)=u\land\operatorname{child}_0(v)=0\)

  • \(\operatorname{find}(v)=u\)\(v\)\(u\) 原树的子树内,即 \(u,v\) 连通。

  • \(\operatorname{father}(v)=u\land\operatorname{child}_0(v)=0\):考虑 makeRoot 后,\(u\) 是 Splay 的根,\(u\) 没有左子树。

    此时,\(v\) 要紧跟在 \(u\) 后面,\(u\rightarrow v\) 中间不能有,\(v\) 的左子树也不能有。

void cut(int u,int v){
    makeRoot(u);
    if(find(v)==u&&t[v].father==u&&!t[v].child[0]){
        t[v].father=t[u].child[1]=0;
    }
}

维护信息操作

单点修改

考虑我们不想上传,因此直接 makeRoot 之后单点修改即可。

void set(int x,int k){
    makeRoot(x);
    t[x].value=k;
}

路径查询

split 之后查询 Splay 根节点信息即可。

int query(int u,int v){
    split(u,v);
    return t[v].sum;
}

时间复杂度

不会势能,懒得学。

总之,是 \(\mathcal O(m\log n)\) 的。

Luogu P3690 动态树(LCT) AC 代码

//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
constexpr const int N=1e5;
int n;
struct LCT{
	struct node{
		int value;
		int sum,father,child[2];
		bool reverse;
	}t[N+1];
	
	bool check(int p){
		return t[t[p].father].child[1]==p;
	}
	bool isRoot(int p){
		return t[t[p].father].child[0]!=p&&t[t[p].father].child[1]!=p;
	}
	void up(int p){
		t[p].sum=t[t[p].child[0]].sum^t[p].value^t[t[p].child[1]].sum;
	}
	void down(int p){
		if(t[p].reverse){
			t[t[p].child[0]].reverse^=1;
			swap(t[t[p].child[0]].child[0],t[t[p].child[0]].child[1]);
			t[t[p].child[1]].reverse^=1;
			swap(t[t[p].child[1]].child[0],t[t[p].child[1]].child[1]);
			t[p].reverse=false;
		}
	}
	void update(int p){
		if(!isRoot(p)){
			update(t[p].father);
		}
		down(p);
	}
	void rotate(int x){
		int y=t[x].father,z=t[y].father;
		bool mode=check(x);
		t[y].child[mode]=t[x].child[!mode];
		t[x].child[!mode]=y;
		if(!isRoot(y)){
			t[z].child[check(y)]=x;
		}
		if(t[y].child[mode]){
			t[t[y].child[mode]].father=y;
		}
		t[y].father=x; 
		t[x].father=z;
		up(y);
		up(x);
	}
	void splay(int x){
		update(x);
		while(!isRoot(x)){
			int p=t[x].father;
			if(isRoot(p)){
				rotate(x);
				break;
			}
			if(check(p)==check(x)){
				rotate(p);
				rotate(x);
			}else{
				rotate(x);
				rotate(x);
			}
		}
	}
	void access(int x){
		for(int p=0;x;p=x,x=t[x].father){
			splay(x);
			t[x].child[1]=p;
			up(x);
		}
	}
	void makeRoot(int x){
		access(x);
		splay(x);
		t[x].reverse^=1;
		swap(t[x].child[0],t[x].child[1]);
	}
	int find(int x){
		access(x);
		splay(x);
		down(x);
		while(t[x].child[0]){
			x=t[x].child[0];
			down(x);
		}
		splay(x);
		return x;
	}
	void split(int u,int v){
		makeRoot(u);
		access(v);
		splay(v);
	}
	int query(int u,int v){
		split(u,v);
		return t[v].sum;
	}
	void link(int u,int v){
		makeRoot(u);
		if(find(v)!=u){
			t[u].father=v; 
		}
	}
	void cut(int u,int v){
		makeRoot(u);
		if(find(v)==u&&t[v].father==u&&!t[v].child[0]){
			t[v].father=t[u].child[1]=0;
		}
	}
	void set(int x,int k){
		makeRoot(x);
		t[x].value=k;
	}
}t;
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	int m;
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int x;
		cin>>x;
		t.set(i,x);
	}
	while(m--){
		int op,x,y;
		cin>>op>>x>>y;
		switch(op){
			case 0:
				cout<<t.query(x,y)<<'\n';
				break;
			case 1:
				t.link(x,y);
				break;
			case 2:
				t.cut(x,y);
				break;
			case 3:
				t.set(x,y);
				break;
		}
	}
	
	cout.flush();
	 
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}
posted @ 2026-02-17 22:35  TH911  阅读(4)  评论(0)    收藏  举报