(笔记)动态树(LCT)

动态树 LCT

本博文对于 LCT 实现的介绍可能不尽详细,有些地方跳过比较多,可以移步其他博客结合理解。

什么是 LCT?

这是一种能用于处理有根森林中各棵树的动态操作的数据结构。它能以 \(O(n\log n)\) 的优秀均摊复杂度完成操作。

具体地,LCT 的核心思想是“原树实链从上到下,对应 Splay 树从左到右”。受到树剖的启发,先把树动态地分成多条链,其中边分为实链和虚链,可以理解为实链是双向的,而虚链是单向的,只有儿子连向父亲的边。LCT 实际上就是把原树通过一些技巧转化为能存储的一颗辅助树(Splay),把所有节点存在多个以深度为 \(key\) 的二叉搜索树(BST)中,BST 内节点通过实边两两相连,并且这些 BST 通过虚边同样相连。

这里我们采用深度分析法,建树过程将节点深度视为要维护的 BST 中的 \(key\)(如果你不了解这种平衡树的维护方式:(简记)FHQ Treap)。然后类似笛卡尔树(本质都是 BST),树的中序遍历就可以得到一个深度递增的序列。

具体地,每个节点 \(u\) 的右儿子代表的子树(如果有的话)就类似重链剖分中重儿子所在的子树(注意这里强调子树,因为真正的重儿子,或者 LCT 中实儿子,可能不与 \(u\) 直接相连),每次从 \(y\) 开始下往上找的过程中建立实链,每经过一条虚链就会丢掉原先实儿子,然后把 \(y\) 子树接在上面。注意我们不希望 LCT 的平均深度变得太大,所以经过虚链时要先 \(splay(x)\) 然后再将 \(y\) 子树接在 \(x\) 上。

知道这个 Splay 是怎么来的,接下来要怎么维护呢?

如何维护 LCT?

这里有几种操作,根据旋转 Treap 和 Splay 的原理,在改变节点位置的时候,这两种维护方式都不会改变 \(key\) 的中序遍历固有顺序(当然改变位置肯定会不符合大根堆性质,但是不需要管这个)。

\(rotate(x)\space splay(x)\)

Splay 经典操作,将辅助树上的 \(x\) 旋转到当前树的根上,这个操作不会影响原树,而且可以改善二叉树形态。具体来说,就是保证该树的中序遍历不变(针对 \(key\))的情况下改变父子关系,把 \(x\) 往上提一级,直到当前 Splay 树根为止(上面可能仍有虚边)。通过这个 \(splay(x)\) 我们保证树的形态使得每次操作是均摊 \(O(\log n)\) 的。

void rotate(int x){
	int y=t[x].fa;
	int z=t[y].fa;
	bool k=(t[y].ch[1]==x);
	if(!isRoot(y))t[z].ch[t[z].ch[1]==y]=x;
	t[x].fa=z;
	t[y].ch[k]=t[x].ch[k^1];
	if(t[x].ch[k^1])t[t[x].ch[k^1]].fa=y;
	t[y].fa=x;
	t[x].ch[k^1]=y;
	pushup(y);
}
void splay(int x){
	int y,z;
	push(x);
	while(!isRoot(x)){
		y=t[x].fa,z=t[y].fa;
		if(!isRoot(y))
			(t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);
		rotate(x);
	}
	pushup(x);
}

具体地,\(rotate(x)\) 每次编码时画图会更好理解,就是 zig 和 zag 操作的简化版。

关于 \(splay(x)\) 内部的分讨,这是因为这一步能改善平衡性。设想节点 \(x,y,z\),满足 \(y\) 对于 \(z\)\(x\) 对于 \(y\) 是同一个方向的儿子(一条链)。这样如果每次只提 \(x\),完成左右操作时就还是一条链的状态,非常不美观。但是如果遇到这种情况先提 \(y\) 再提 \(x\),就可以一定程度上减少深度,维护平衡性,具体过程请读者自行模拟。

\(access(x)\)

在原树上建一条实链,使得链头是 \(root\),链尾是 \(x\)。如何实现?

只需要在下一个实链把链尾提到实链链头,把这个实链的链头接到下一个实链链头的右儿子即可(这样做是为了改善平衡性,避免深度过大)。如果这个实链为空,那么下一个实链链头的右儿子也为空,因为链尾得是 \(x\)

为什么要是右儿子?因为辅助树上虚链代表了原树中的深度偏序关系,如果一个节点通过虚链是另一个节点的父亲,那么这个节点深度肯定比另一个节点浅,显然就应该接在右儿子上。

void access(int x){
	for(int child=0;x;child=x,x=t[x].fa){
		splay(x);
		rc=child;
		pushup(x);
	}
}

\(makeroot(x)\)

这个操作能够改变原树形态。注意,这里的改变形态是指改变根节点,但和换根 DP 一样,如果将新树和原树都看做无根树,那么它们实际上应该是等价的(同构的)。

该操作有三步:\(access(x)\rightarrow splay(x)\rightarrow reverse(x)\)

第一步和第二步是为了创造辅助树上以 \(x\) 为根的 Splay,第三步则是主要操作。想象一下树上如果要将 \(x\) 提为根,那么 \(root\rightarrow x\) 这段路径上的节点深度的递增顺序都应该变成递减顺序。理解了这一步以后,我们只需要在 Splay 树上把实链上所有左右儿子都交换就可以完成这种关系的改变。

void makeroot(int x){
	access(x);
	splay(x);
	reverse(x);
}

具体实现

主要操作如上,剩下的就是根据性质变形得到的求解问题的方法。注意如果要查询路径 \(x\rightarrow y\) 上的信息,那么需要执行一次 \(split(x,y)\) 操作,在原树上以 \(x\) 为起点,\(y\) 为终点生成一条实链,然后路径上的信息就是这条实链上的所有信息了。

下面给出P3690 【模板】动态树(LCT)的完整代码,具体细节参照代码:

#include<bits/stdc++.h>
using namespace std;
const int N=3e5+5;
struct Node{int ch[2],sum,val,tg,fa;}t[N];
#define lc t[x].ch[0]
#define rc t[x].ch[1]
bool isRoot(int x){
	int g=t[x].fa;
	return t[g].ch[0]!=x&&t[g].ch[1]!=x;
}
void pushup(int x){
	t[x].sum=t[x].val^t[lc].sum^t[rc].sum;
}
void reverse(int x){
	if(!x)return ;
	swap(lc,rc);
	t[x].tg^=1;
}
void pushdown(int x){
	if(t[x].tg){
		reverse(lc);
		reverse(rc);
		t[x].tg=0;
	}
}
void push(int x){
	if(!isRoot(x))push(t[x].fa);
	pushdown(x);
}
void rotate(int x){
	int y=t[x].fa;
	int z=t[y].fa;
	bool k=(t[y].ch[1]==x);
	if(!isRoot(y))t[z].ch[t[z].ch[1]==y]=x;
	t[x].fa=z;
	t[y].ch[k]=t[x].ch[k^1];
	if(t[x].ch[k^1])t[t[x].ch[k^1]].fa=y;
	t[y].fa=x;
	t[x].ch[k^1]=y;
	pushup(y);
}
void splay(int x){
	int y,z;
	push(x);
	while(!isRoot(x)){
		y=t[x].fa,z=t[y].fa;
		if(!isRoot(y))
			(t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);
		rotate(x);
	}
	pushup(x);
}
void access(int x){
	for(int child=0;x;child=x,x=t[x].fa){
		splay(x);
		rc=child;
		pushup(x);
	}
}
void makeroot(int x){
	access(x);
	splay(x);
	reverse(x);
}
void split(int x,int y){
	makeroot(x);
	access(y);
	splay(y);
}
void link(int x,int y){
	makeroot(x);
	t[x].fa=y;
}
void cut(int x,int y){
	split(x,y);
	if(t[y].ch[0]!=x||rc)return ;
	t[y].ch[0]=t[x].fa=0;
	pushup(y);
}
int findroot(int x){
	access(x);splay(x);
	while(lc)pushdown(x),x=lc;
	return x;
}
#undef lc
#undef rc
int n,m;
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>t[i].val,t[i].sum=t[i].val;
	while(m--){
		int opt,a,b;
		cin>>opt>>a>>b;
		if(!opt)split(a,b),cout<<t[b].sum<<'\n';
		else if(opt==1){if(findroot(a)!=findroot(b))link(a,b);}
		else if(opt==2)cut(a,b);
		else splay(a),t[a].val=b;
	}
	return 0;
}

Q & A

  • Q:为什么 \(access(x)\) 的过程破坏了链的形态(可能有一个节点同时存在左右儿子)却不影响后续操作?
  • A:并没有破坏链的形态,该问题属于结构混淆,辅助树上的一棵 Splay 维护的就是一条实链中的所有节点。
  • Q:为什么 \(makeroot(x)\) 不可以只将 \(root\) 的左右儿子互换,而是要全体 \(reverse\)
  • A:参照前文讲解,手动模拟原树提根,只有深度都倒过来才不会影响原树结构。
  • Q:为什么 \(cut(x,y)\) 操作中 \(split(x,y)\) 操作中 \(splay(y)\) 可能在上升过程中先 \(rotate\) 其父节点再 \(rotate\) 它自己导致它的左儿子不是 \(x\) 而不用特殊判断?
  • A:这也是直接写 Splay 和 LCT 的一大区别,因为 \(cut\) 操作保证了 \(x,y\) 是原树上相邻节点,所以建实链,链上 \(x,y\) 之间必然不存在其他节点,也就不会有上面的问题了。
  • Q:为什么 \(findroot(x)\) 操作可以直接判断是否存在左儿子再 pushdown(x)
  • A:?pushdown(x) 更新的是 \(x\) 的儿子的信息,和 \(x\) 没关系。
  • Q:有哪些容易写错的地方?
  • A:\(rotate(x)\) 内判断 \(y\) 是否为根、查询路径信息 \(split(x,y)\) 后没有意识到 \(x\) 只是在 \(y\) 左子树内而不一定是 \(y\) 的左儿子,直接返回了 \(x\) 的信息等。

LCT 经典应用

LCT 维护子树信息(虚子树)

题解 P4219 【[BJOI2014]大融合】

本题有非常好的题解解释有关此 trick 的种种细节,故不在叙述。一个容易忽略的点,在模板问题的 \(link(x,y)\) 操作中,我们把 \(x\) 接到 \(y\) 的一个虚儿子上,这并不会影响 \(y\) 的任何维护值所以可以不执行makeroot(y)。但是本题则不然,如果不执行,那么 \(y\) 及其所有祖先的信息都没有得到及时更新,所以需要多写一步,这是值得注意的。

定根 LCT

无 makeroot 这一特点建立在原树的形态结构唯一的基础上。如果一个点在全局修改中可能有不止一个父亲,那么就不符合该条件。经典应用是SP16549 QTREE6 - Query on a tree VI的 LCT 解法。在该问题中,由于建模的独特性,致使不能改变原树形态,所以可以采取无 makeroot 的 LCT 并且不用担心父亲节点矛盾的问题。

边权 LCT 动态维护最小生成树

边权 LCT 解决方法:将每条边多加一个虚点,边权赋在点权上连接即可。(注意\(cut\) 操作中也需要删除虚点与实点的边)。

动态维护最小生成树:每次查询动态树链信息,找到最大边权,如果替换掉完全更优那么先断掉原来更劣的边,然后直接连上新边。

P4172 [WC2006] 水管局长

考虑到删除困难,加入容易,时光倒流维护最小生成树即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+5,INF=1e9+1;
int n,m,q;
struct edge{int u,v,w;}op[N];
struct node{int ch[2],val,mx,id,fa;bool tg;}t[N*2];
inline bool isRoot(int x){
	int f=t[x].fa;
	return t[f].ch[0]!=x&&t[f].ch[1]!=x;
}
void pushup(int p){
	t[p].mx=max(t[p].val,max(t[t[p].ch[0]].mx,t[t[p].ch[1]].mx));
	if(t[p].mx==t[p].val)t[p].id=p;
	else if(t[p].mx==t[t[p].ch[0]].mx)t[p].id=t[t[p].ch[0]].id;
	else t[p].id=t[t[p].ch[1]].id;
}
void mktag(int p){
	if(!p)return ;
	t[p].tg^=1;
	swap(t[p].ch[0],t[p].ch[1]);
}
void pushdown(int p){
	if(t[p].tg){
		mktag(t[p].ch[0]);
		mktag(t[p].ch[1]);
		t[p].tg=0;
	}
}
void push(int p){
	if(!isRoot(p))push(t[p].fa);
	pushdown(p);
}
void rotate(int x){
	int y=t[x].fa,z=t[y].fa;
	if(!isRoot(y))t[z].ch[t[z].ch[1]==y]=x;
	t[y].fa=x;t[x].fa=z;
	bool k=(t[y].ch[1]==x);
	if(t[x].ch[k^1])t[t[x].ch[k^1]].fa=y;
	t[y].ch[k]=t[x].ch[k^1];
	t[x].ch[k^1]=y;
	pushup(y);
}
void splay(int x){
	int y,z;
	push(x);
	while(!isRoot(x)){
		y=t[x].fa,z=t[y].fa;
		if(!isRoot(y))
			((t[z].ch[1]==y)^(t[y].ch[1]==x))?rotate(x):rotate(y);
		rotate(x);
	}
	pushup(x);
}
void access(int x){
	for(int child=0;x;child=x,x=t[x].fa){
		splay(x);
		t[x].ch[1]=child;
		pushup(x);
	}
}
void makeroot(int x){
	access(x);
	splay(x);
	mktag(x);
}
int findroot(int x){
	access(x);splay(x);
	while(t[x].ch[0])pushdown(x),x=t[x].ch[0];
	return x;
}
void split(int x,int y){
	makeroot(x);
	access(y);
	splay(y);
}
void link(int x,int y){
	makeroot(x);
	t[x].fa=y;
}
void cut(int x,int y){
	split(x,y);
	assert(t[y].ch[0]==x);
	t[y].ch[0]=0,t[x].fa=0;
	pushup(y);
}
pair<int,int>query(int x,int y){
	if(findroot(x)!=findroot(y))return make_pair(INF,0);
	split(x,y);
	int pos=y;
	return make_pair((t[y].mx?t[y].mx:INF),t[y].id);
}
map<int,map<int,int> >vis;
map<int,map<int,bool> >mp;
int X[N],Y[N],W[N],ans[N];
void Link(int w){
	pair<int,int>res=query(X[w],Y[w]);
	if(res.first>W[w]&&res.second>n){
		int id=res.second-n;
		cut(X[id],id+n);
		cut(id+n,Y[id]);
	}
	if(res.first>W[w]){
		link(w+n,X[w]);
		link(Y[w],w+n);
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m>>q;
	for(int i=1;i<=m;i++)
		cin>>X[i]>>Y[i]>>W[i],
		t[i+n].mx=t[i+n].val=W[i],
		t[i+n].id=i+n,
		vis[X[i]][Y[i]]=vis[Y[i]][X[i]]=i;
	for(int i=1;i<=q;i++){
		cin>>op[i].w>>op[i].u>>op[i].v;
		if(op[i].w==2)mp[op[i].u][op[i].v]=mp[op[i].v][op[i].u]=1;
	}
	for(int i=1;i<=m;i++)
		if(!mp[X[i]][Y[i]])
			Link(i);
	for(int i=q;i>=1;i--){
		if(op[i].w==2)Link(vis[op[i].u][op[i].v]);
		else ans[i]=query(op[i].u,op[i].v).first;
	}
	for(int i=1;i<=q;i++)
		if(op[i].w==1)cout<<ans[i]<<'\n';
	return 0;
}

P2387 [NOI2014] 魔法森林

最小生成树维护:比上一题多个扫描线,这个题对 \(a,b\) 中任意一维做一个扫描线,然后另一维直接动态最小生成树,记录路径信息是路径上边权最大的点及其编号,如果替换更优那么直接替换,然后找到 \(1\)\(m\) 可行路径上最大边权 \(+\) 扫描线所处位置就是本次答案,将所有答案取 \(\min\) 即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=5e4+5,M=1e5+5,INF=1e9;
struct node{int ch[2],fa,mx,id,val;bool rev;}t[N+M];
inline bool isRoot(int x){
	int f=t[x].fa;
	return t[f].ch[0]!=x&&t[f].ch[1]!=x;
}
void pushup(int x){
	t[x].mx=max(t[t[x].ch[0]].mx,t[t[x].ch[1]].mx);
	t[x].mx=max(t[x].mx,t[x].val);
	if(t[x].mx==t[x].val)t[x].id=x;
	else if(t[x].mx==t[t[x].ch[0]].mx)t[x].id=t[t[x].ch[0]].id;
	else t[x].id=t[t[x].ch[1]].id;
}
void rev(int x){
	if(!x)return ;
	t[x].rev^=1;
	swap(t[x].ch[0],t[x].ch[1]);
}
void pushdown(int x){
	if(t[x].rev){
		rev(t[x].ch[0]);
		rev(t[x].ch[1]);
		t[x].rev=0;
	}
}
void push(int x){
	if(!isRoot(x))push(t[x].fa);
	pushdown(x);
}
void rotate(int x){
	int y=t[x].fa,z=t[y].fa;
	if(!isRoot(y))t[z].ch[t[z].ch[1]==y]=x;
	t[x].fa=z,t[y].fa=x;
	bool k=(t[y].ch[1]==x);
	if(t[x].ch[k^1])t[t[x].ch[k^1]].fa=y;
	t[y].ch[k]=t[x].ch[k^1];
	t[x].ch[k^1]=y;
	pushup(y);
}
void splay(int x){
	int y,z;
	push(x);
	while(!isRoot(x)){
		y=t[x].fa,z=t[y].fa;
		if(!isRoot(y))
			((t[z].ch[1]==y)^(t[y].ch[1]==x))?rotate(x):rotate(y);
		rotate(x);
	}
	pushup(x);
}
void access(int x){
	for(int child=0;x;child=x,x=t[x].fa){
		splay(x);
		t[x].ch[1]=child;
		pushup(x);
	}
}
void makeroot(int x){
	access(x);
	splay(x);
	rev(x);
}
int findroot(int x){
	access(x);splay(x);
	while(t[x].ch[0]||t[x].ch[1]){
		pushdown(x);
		if(t[x].ch[0])x=t[x].ch[0];
		else break;
	}
	return x;
}
void cut(int x,int y){
	makeroot(x);
	access(y);splay(y);
	if(t[y].ch[0]==x){
		t[y].ch[0]=0;
		t[x].fa=0;
		pushup(y);
	}
}
void link(int x,int y){
	makeroot(x);
	t[x].fa=y;
}
pair<int,int>query(int x,int y){
	if(findroot(x)!=findroot(y))
		return make_pair(INF,0);
	makeroot(x);
	access(y);splay(y);
	return make_pair(t[y].mx,t[y].id);
}
int n,m,X[N+M],Y[N+M],ans=INF,cnte,ncnt;
struct edge{int u,v,a,b;}e[M];
bool cmp(edge x,edge y){return x.b<y.b;}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;ncnt=n;
	for(int i=1;i<=n;i++)
		t[i].id=i;
	for(int i=1;i<=m;i++)
		cin>>e[i].u>>e[i].v>>e[i].a>>e[i].b;
	sort(e+1,e+1+m,cmp);
	for(int i=1;i<=m;i++){
		if(e[i].u==e[i].v)continue;
		pair<int,int>res=query(e[i].u,e[i].v);
		if(res.first<=e[i].a)continue;
		if(res.second){
			cut(X[res.second],res.second);
			cut(res.second,Y[res.second]);
			cnte--;
		}
		t[++ncnt].val=e[i].a;
		t[ncnt].mx=t[ncnt].val;
		t[ncnt].id=ncnt;
		X[ncnt]=e[i].u,Y[ncnt]=e[i].v;
		link(e[i].u,ncnt);link(ncnt,e[i].v);
		cnte++;
		if(findroot(1)==findroot(n))ans=min(ans,query(1,n).first+e[i].b);
	}
	cout<<(ans>=INF?-1:ans);
	return 0;
}

边权 LCT 动态维护连通块个数

P5385 [Cnoi2019] 须臾幻境

说实话虽然是笛卡尔积题,但是第一次没有看原代码自己默写一遍 LCT 板子 + 可持久化线段树居然没怎么出错调了十几分钟就过了,挺惊喜的。

trick:\(\text{连通块数}=\text{点数}-\text{森林边数}\)

本题每次询问的森林边数为原有边数 \(r-l+1\) 减去不存在的边数量 \(c\),答案即为 \(n-(r-l+1-c)\)。如何统计 \(c\)?用 LCT 从小到大加边动态维护最大生成树(边权为边编号),套一个可持久化线段树 \(pos\) 信息表示第 \(pos\) 条边是否被删除,然后查询直接查 \(1\dots r\) 的所有边加入后,\([l,r]\) 有多少条边已经不存在了,这个就是 \(c\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=4e5+5,INF=1e9;
int n,m,q,Pas,ans,rt[N];

struct Sgt{
	int ncnt;
	struct node{int sum,lc,rc;}t[N<<5];
	#define ls t[p].lc
	#define rs t[p].rc
	#define mid ((l+r)>>1)
	void modify(int o,int &p,int l,int r,int pos){
		t[p=++ncnt]=t[o];
		t[p].sum++;
		if(l==r)return;
		if(pos<=mid)modify(t[o].lc,ls,l,mid,pos);
		else modify(t[o].rc,rs,mid+1,r,pos);
	}
	int query(int o,int p,int l,int r,int L,int R){
		if(L<=l&&r<=R)return t[p].sum-t[o].sum;
		int res=0;
		if(L<=mid)res+=query(t[o].lc,ls,l,mid,L,R);
		if(R>mid)res+=query(t[o].rc,rs,mid+1,r,L,R);
		return res;
	}
	#undef ls
	#undef rs
	#undef mid
}T;

int id[N],ncnt,mp[N],U[N],V[N];
struct Node{int ch[2],fa,val,mn,id;bool rev;}t[N<<1];
#define ls t[x].ch[0]
#define rs t[x].ch[1]
void pushup(int x){
	t[x].mn=min(t[x].val,min(t[ls].mn,t[rs].mn));
	if(t[x].mn==t[x].val)t[x].id=x;
	else if(t[x].mn==t[ls].mn)t[x].id=t[ls].id;
	else t[x].id=t[rs].id;
}
inline bool isRoot(int x){
	int f=t[x].fa;
	return t[f].ch[0]!=x&&t[f].ch[1]!=x;
}
void rotate(int x){
	int y=t[x].fa,z=t[y].fa;
	if(!isRoot(y))t[z].ch[t[z].ch[1]==y]=x;
	t[x].fa=z;t[y].fa=x;
	bool k=(t[y].ch[1]==x);
	if(t[x].ch[k^1])
		t[t[x].ch[k^1]].fa=y;
	t[y].ch[k]=t[x].ch[k^1];
	t[x].ch[k^1]=y;
	pushup(y);
}
inline void reverse(int x){
	if(!x)return ;
	t[x].rev^=1;
	swap(ls,rs);
}
void pushdown(int x){
	if(t[x].rev){
		if(ls)reverse(ls);
		if(rs)reverse(rs);
		t[x].rev=0;
	}
}
void push(int x){
	if(!isRoot(x))push(t[x].fa);
	pushdown(x);
}
void splay(int x){
	int y,z;
	push(x);
	while(!isRoot(x)){
		y=t[x].fa,z=t[y].fa;
		if(!isRoot(y))
			((t[z].ch[1]==y)^(t[y].ch[1]==x))?rotate(x):rotate(y);
		rotate(x); 
	}
	pushup(x);
}
void access(int x){
	for(int child=0;x;child=x,x=t[x].fa){
		splay(x);
		rs=child;
		pushup(x);
	}
}
void makeroot(int x){
	access(x);
	splay(x);
	reverse(x);
}
int findroot(int x){
	access(x);splay(x);
	while(ls)
		pushdown(x),x=ls;
	return x;
}
void cut(int x,int y){
	makeroot(y);
	access(x);
	splay(x);
	ls=0;t[y].fa=0;
	pushup(x);
}
void link(int x,int y){
	makeroot(x);
	t[x].fa=y;
}
int query(int x,int y){
	makeroot(y);
	access(x);
	splay(x);
	rs=0;pushup(x);	
	return t[x].id;
}
#undef ls
#undef rs
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m>>q>>Pas;ncnt=n;
	for(int i=0;i<=n+m;i++)
		t[i].id=i,t[i].mn=t[i].val=INF;
	for(int i=1;i<=m;i++){
		int u,v;cin>>u>>v;
		U[i]=u,V[i]=v;
		id[i]=++ncnt;
		mp[ncnt]=i;
		t[ncnt].val=t[ncnt].mn=i;
		if(u!=v){
			if(findroot(u)==findroot(v)){
				int res=query(u,v);
				int u1=U[mp[res]],v1=V[mp[res]];
				cut(u1,res);cut(res,v1);
				T.modify(rt[i-1],rt[i],1,m,mp[res]);
			}
			else rt[i]=rt[i-1];
			link(id[i],v),link(u,id[i]);
		}
		else T.modify(rt[i-1],rt[i],1,m,i);
	}
	for(int i=1;i<=q;i++){
		int l,r;cin>>l>>r;
		if(Pas>0){
			l=(l+1ll*Pas*ans)%m+1;
			r=(r+1ll*Pas*ans)%m+1;
		}
		if(l>r)swap(l,r);
		ans=n-(r-l+1-T.query(0,rt[r],1,m,l,r));
		cout<<ans<<'\n';
	}
	return 0;
}

P9598 [JOI Open 2018] 山体滑坡 / Collapse

这题应该放在线段树分治的?这个思路挺板的就是,虽然是有点笛卡尔积题但是真的难写,封装完几十行的 LCT 还要搞两个分讨。LCT 本来常数就大,这个东西用上述维护连通块个数的 trick,写起来简直就是赤石,写了 \(\text{6.1KB}\),还是没能超过之前写树套树还是什么东西的构式代码量。

考虑把加边删边套一个线段树分治变成加边撤销,然后求的就是只有 \((u,v)\) 满足 \(\max(u,v)\le p\)\(\min(u,v)\geq p+1\) 的边的连通块数量。考虑到 \([1,p]\)\([p+1,n]\) 必定不连通,可以建两棵 LCT,\(T_1\) 维护边权为 \(\max(u,v)\) 的最小生成树,\(T_2\) 维护边权为 \(\min(u,v)\) 的最大生成树,然后原生成树的边数就是两棵 LCT 的边数和,考虑到还有 \(p\) 的限制,套一个树状数组,每次 LinkCut 对应修改边权下标的信息即可。时间复杂度 \(O(n\log^2 n)\),难写且常数大\fn,也是赤了一晚上石。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
const int INF=1e9;
const int N=2e5+5;
int n,m,qn,cnte;
struct BIT{
	int tp,stk[N<<2],V[N<<2];
	int av[N];
	inline int lowbit(int x){return x&-x;}
	void add(int p,int x,bool und){
		if(!und)stk[++tp]=p,V[tp]=x;
		for(int i=p;i<=n;i+=lowbit(i))av[i]+=x;
	}
	int que(int p){
		int res=0;
		for(int i=p;i;i-=lowbit(i)){res+=av[i];}
		return res;
	}
	void undo(int rcd){
		while(tp>rcd){
			int p=stk[tp],v=V[tp];
			add(p,-v,1);
			tp--;
		}
	}
}t1,t2;
int mp[N],U[N],V[N];
int ncnt,id[N];
struct LCT{
	PII stk[N<<3];int tp;
	bool add[N<<3];
	struct Node{
		int ch[2],fa;
		int valn,valx;
		int mn,mx;
		int idn,idx;
		bool rev;
	}t[N<<1];
	#define ls t[x].ch[0]
	#define rs t[x].ch[1]
	void pushup(int x){
		t[x].mn=min(t[x].valn,min(t[ls].mn,t[rs].mn));
		if(t[x].mn==t[x].valn)t[x].idn=x;
		else if(t[x].mn==t[ls].mn)t[x].idn=t[ls].idn;
		else t[x].idn=t[rs].idn;
		
		t[x].mx=max(t[x].valx,max(t[ls].mx,t[rs].mx));
		if(t[x].mx==t[x].valx)t[x].idx=x;
		else if(t[x].mx==t[ls].mx)t[x].idx=t[ls].idx;
		else t[x].idx=t[rs].idx;
	}
	inline bool isRoot(int x){
		int f=t[x].fa;
		return t[f].ch[0]!=x&&t[f].ch[1]!=x;
	}
	void rotate(int x){
		int y=t[x].fa,z=t[y].fa;
		if(!isRoot(y))t[z].ch[t[z].ch[1]==y]=x;
		t[x].fa=z;t[y].fa=x;
		bool k=(t[y].ch[1]==x);
		if(t[x].ch[k^1])
			t[t[x].ch[k^1]].fa=y;
		t[y].ch[k]=t[x].ch[k^1];
		t[x].ch[k^1]=y;
		pushup(y);
	}
	inline void reverse(int x){
		if(!x)return ;
		t[x].rev^=1;
		swap(ls,rs);
	}
	void pushdown(int x){
		if(t[x].rev){
			if(ls)reverse(ls);
			if(rs)reverse(rs);
			t[x].rev=0;
		}
	}
	void push(int x){
		if(!isRoot(x))push(t[x].fa);
		pushdown(x);
	}
	void splay(int x){
		int y,z;
		push(x);
		while(!isRoot(x)){
			y=t[x].fa,z=t[y].fa;
			if(!isRoot(y))
				((t[z].ch[1]==y)^(t[y].ch[1]==x))?rotate(x):rotate(y);
			rotate(x); 
		}
		pushup(x);
	}
	void access(int x){
		for(int child=0;x;child=x,x=t[x].fa){
			splay(x);
			rs=child;
			pushup(x);
		}
	}
	void makeroot(int x){
		access(x);
		splay(x);
		reverse(x);
	}
	int findroot(int x){
		access(x);splay(x);
		while(ls)
			pushdown(x),x=ls;
		return x;
	}
	void cut(int x,int y,bool und){
		if(!und)stk[++tp]=make_pair(x,y),add[tp]=0;
		makeroot(y);
		access(x);
		splay(x);
		ls=0;t[y].fa=0;
		pushup(x);
	}
	void link(int x,int y,bool und){
		if(!und)stk[++tp]=make_pair(x,y),add[tp]=1;
		makeroot(x);
		t[x].fa=y;
	}
	PII query(int x,int y,bool tf){
		makeroot(y);
		access(x);
		splay(x);
		rs=0;pushup(x);	
		return make_pair(tf?t[x].mx:t[x].mn,
			tf?t[x].idx:t[x].idn);
	}
	void undo(int rcd){
		while(tp>rcd){
			PII x=stk[tp];
			int u=x.first,v=x.second;
			if(add[tp])cut(u,v,1);
			else link(u,v,1);
			tp--;
		}
	}
	#undef ls
	#undef rs
}T1,T2;
map<int,map<int,int> >mpL;
vector<int>G[N<<2];
vector<int>ID[N];
int ans[N],q[N],leftq[N<<2];
#define ls p<<1
#define rs p<<1|1
#define mid ((l+r)>>1)
void update(int p,int l,int r,int L,int R,int x){
	if(L<=l&&r<=R){
		G[p].emplace_back(x);
		return ;
	}
	if(L<=mid)update(ls,l,mid,L,R,x);
	if(R>mid)update(rs,mid+1,r,L,R,x);
}
void modify(int p,int l,int r,int pos){
	leftq[p]++;
	if(l==r)return ;
	if(pos<=mid)modify(ls,l,mid,pos);
	else modify(rs,mid+1,r,pos);
}
void answer(int p,int l,int r){
	if(!leftq[p])return ;
	int rcd1=T1.tp,rcd2=T2.tp;
	int Rcd1=t1.tp,Rcd2=t2.tp;
	for(int i:G[p]){
		int u=U[i],v=V[i];
		if(u!=v){
			if(T1.findroot(u)==T1.findroot(v)){
				PII resP=T1.query(u,v,1);
				if(resP.first>max(u,v)){
					int res=resP.second;
					int u1=U[mp[res]],v1=V[mp[res]];
					T1.cut(u1,res,0);T1.cut(res,v1,0);
					t1.add(max(u1,v1),-1,0);
					T1.link(id[i],v,0),T1.link(u,id[i],0);
					t1.add(max(u,v),1,0);
				}
			}
			else T1.link(id[i],v,0),T1.link(u,id[i],0),
				t1.add(max(u,v),1,0);
			
			if(T2.findroot(u)==T2.findroot(v)){
				PII resP=T2.query(u,v,0);
				if(resP.first<min(u,v)){
					int res=resP.second;
					int u1=U[mp[res]],v1=V[mp[res]];
					T2.cut(u1,res,0);T2.cut(res,v1,0);
					t2.add(min(u1,v1),-1,0);
					T2.link(id[i],v,0),T2.link(u,id[i],0);
					t2.add(min(u,v),1,0);
				}
			}
			else T2.link(id[i],v,0),T2.link(u,id[i],0),
				t2.add(min(u,v),1,0);
		}
	}
	if(l==r){
		for(int v:ID[l]){
			int id=v,u=q[v];
			ans[id]=t1.que(u);
			ans[id]+=t2.que(n)-t2.que(u);
			ans[id]=n-ans[id];
		}
		T1.undo(rcd1);T2.undo(rcd2);
		t1.undo(Rcd1);t2.undo(Rcd2);
		return ;
	}
	answer(ls,l,mid);
	answer(rs,mid+1,r);
	T1.undo(rcd1);T2.undo(rcd2);
	t1.undo(Rcd1);t2.undo(Rcd2);
}
#undef ls
#undef rs
#undef mid
int rcU[N],rcV[N];
bool rco[N],Left[N];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m>>qn;ncnt=n;
	for(int i=0;i<=n+m;i++){
		T1.t[i].idn=T1.t[i].idx=i;
		T1.t[i].mn=INF,T1.t[i].mx=-INF;
		T1.t[i].valn=INF,T1.t[i].valx=-INF;
		
		T2.t[i].idn=T2.t[i].idx=i;
		T2.t[i].mn=INF,T2.t[i].mx=-INF;
		T2.t[i].valn=INF,T2.t[i].valx=-INF;
	}
	for(int i=1;i<=m;i++){
		int u,v;bool o;cin>>o>>u>>v;
		u++;v++;
		rcU[i]=u,rcV[i]=v,rco[i]=o;
		PII X=make_pair(u,v);
		if(!o)mpL[u][v]=mpL[v][u]=i,Left[i]=1;
		else {
			U[++cnte]=u;V[cnte]=v;
			ncnt++;Left[mpL[u][v]]=0;
			T1.t[ncnt].valx=T1.t[ncnt].mx=max(u,v);
			T2.t[ncnt].valn=T2.t[ncnt].mn=min(u,v);
			T1.t[ncnt].idx=T2.t[ncnt].idn=ncnt;
			mp[ncnt]=cnte;id[cnte]=ncnt;
			update(1,1,m,mpL[u][v],i-1,cnte);
		}
	}
	for(int i=1;i<=m;i++)
		if(!rco[i]&&Left[i]){
			int u=rcU[i],v=rcV[i];
			U[++cnte]=u;V[cnte]=v;
			ncnt++;
			T1.t[ncnt].valx=T1.t[ncnt].mx=max(u,v);
			T2.t[ncnt].valn=T2.t[ncnt].mn=min(u,v);
			T1.t[ncnt].idx=T2.t[ncnt].idn=ncnt;
			mp[ncnt]=cnte;id[cnte]=ncnt;
			update(1,1,m,i,m,cnte);
			Left[i]=0;
		}
	for(int i=1;i<=qn;i++){
		int day,u;cin>>day>>u;
		day++;u++;
		q[i]=u;
		modify(1,1,m,day);
		ID[day].emplace_back(i);
	}
	answer(1,1,m);
	for(int i=1;i<=qn;i++)
		cout<<ans[i]<<'\n';
	return 0;
}

全局平衡二叉树(静态 LCT)

题解 P4115 【Qtree4】

科技。该结构本质上是重链剖分+实链剖分的应用。

对树进行重链剖分。对于每条重链,每次 build 求它的加权重心(每个点的点权是轻儿子 \(siz+1\)),然后递归建树。

每次从儿子跳到父亲的子树大小就至少翻倍,可以稍微分类讨论一下。如果经过一条轻边,那么子树大小至少翻倍。如果经过一条重链内部的边(BST 上的边),那么由于每次找重心进行 build,子树大小也是翻倍的。这样我们的树高就是 \(O(\log n)\) 的。

全局平衡二叉树可以帮我们解决许多问题,让我们可以摆脱树剖 \(O(\log ^2 n)\) 的单次操作,变成 \(O(\log n)\)

P4751 【模板】动态 DP(加强版)

但是我不将原树二叉化,只纯朴地建一颗全局平衡二叉树又如何呢?我们在 Qtree4 中,由于维护信息的特殊性,要额外在树上开一些线段树节点。但是一般情况下,如本题中不需要按一定秩序开出这些节点,直接朴素建树即可。需要注意的是,递归轻儿子建树时与上面不同,每次 dfs 前都要重置链首(因为这个调了 1h)。

关于调错

了解 LCT 代码错误的最好方式是知道 Splay 树的构造。因此,不妨写一个打印函数帮助你更好地调试。

void pr(int x){
	if(lc)pr(lc);
	cout<<x<<' ';
	if(rc)pr(rc);
}
void print(int x){
	pr(x);
	cout<<'\n';
}

当然啦实际上如果你的 LCT 没出什么离谱大锅,多数时候程序运行卡住了都是因为连边出现了环,可以尝试在每个 \(link(x,y)\)\(cut(x,y)\) 操作中输出连边删边的提示然后判断哪里出了问题。

关于 tag(血的教训)

P4332 [SHOI2014] 三叉神经树

本题按照标准的 \(O(n\log n)\) 的思路,需要在 Splay 每个节点维护两个信息。第一个是当前子树内深度最深且 \(val\) 不为 \(1\) 的点,第二个是当前子树内深度最深且 \(val\) 不为 \(2\) 的点。这两个信息在打 tag 修改的时候需要进行交换。 需要注意的是,本题不能直接使用覆盖 tag,这样可能会导致被两个不同的 tag 同时覆盖以后本来应该没有任何效果,但是 pushdown 判断自动交换了这两个信息。

解决方法是用一个反转 tag(用异或维护信息),在翻转两次时自动视为没有翻转。

本题调错耗时 3days。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int dep[N*3],val[N*3];
struct Node{int ch[2],fa,lg[2],tg;}t[3*N];
void gv0(int &a,int b){if(dep[b]>dep[a]&&val[b]!=1)a=b;}
void gv1(int &a,int b){if(dep[b]>dep[a]&&val[b]!=2)a=b;}
#define lc t[x].ch[0]
#define rc t[x].ch[1]
bool isRoot(int x){
	int g=t[x].fa;
	return t[g].ch[0]!=x&&t[g].ch[1]!=x;
}
void pushup(int x){
	t[x].lg[0]=0;
	gv0(t[x].lg[0],x);
	gv0(t[x].lg[0],t[lc].lg[0]);
	gv0(t[x].lg[0],t[rc].lg[0]);
	t[x].lg[1]=0;
	gv1(t[x].lg[1],x);
	gv1(t[x].lg[1],t[lc].lg[1]);
	gv1(t[x].lg[1],t[rc].lg[1]);
}
void mktag(int x,int v){
	if(!x)return ;
	swap(t[x].lg[0],t[x].lg[1]);
	val[x]^=3;
	t[x].tg+=v;
}
void pushdown(int x){
	if(t[x].tg){
		mktag(lc,t[x].tg);
		mktag(rc,t[x].tg);
		t[x].tg=0;
	}
}
void push(int x){
	if(!isRoot(x))push(t[x].fa);
	pushdown(x);
}
void rotate(int x){
	int y=t[x].fa;
	int z=t[y].fa;
	bool k=(t[y].ch[1]==x);
	if(!isRoot(y))t[z].ch[t[z].ch[1]==y]=x;
	t[x].fa=z;
	t[y].ch[k]=t[x].ch[k^1];
	if(t[x].ch[k^1])t[t[x].ch[k^1]].fa=y;
	t[y].fa=x;
	t[x].ch[k^1]=y;
	pushup(y);
}
void splay(int x){
	int y,z;
	push(x);
	while(!isRoot(x)){
		y=t[x].fa,z=t[y].fa;
		if(!isRoot(y))
			(t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);
		rotate(x);
	}
	pushup(x);
}
void access(int x){
	for(int child=0;x;child=x,x=t[x].fa){
		splay(x);
		rc=child;
		pushup(x);
	}
}
#undef lc
#undef rc
int n,m;
vector<int>G[N];
void dfs(int u){
	if(val[u])return ;
	for(int v:G[u]){
		dep[v]=dep[u]+1,dfs(v);
		val[u]+=(val[v]>1);
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++){
		int x1,x2,x3;
		cin>>x1>>x2>>x3;
		G[i].push_back(x1);
		G[i].push_back(x2);
		G[i].push_back(x3);
		t[x1].fa=t[x2].fa=t[x3].fa=i;
	}
	for(int i=n+1;i<=3*n+1;i++)
		cin>>val[i],val[i]=(val[i]?2:1),
		gv0(t[i].lg[0],i),gv1(t[i].lg[1],i);
	dep[1]=1;dfs(1);
	cin>>m;
	while(m--){
		int x;
		cin>>x;
		access(x);splay(x);
		if(val[x]==1){
			if(t[x].lg[0]){
				int rt=t[x].lg[0];
				splay(rt);
				val[rt]++;
				gv0(t[rt].lg[0],rt),gv1(t[rt].lg[1],rt);
				mktag(t[rt].ch[1],1);
			}
			else mktag(x,1);
		}
		else {
			if(t[x].lg[1]){
				int rt=t[x].lg[1];
				splay(rt);
				val[rt]--;
				gv0(t[rt].lg[0],rt),gv1(t[rt].lg[1],rt);
				mktag(t[rt].ch[1],-1);
			}
			else mktag(x,-1);
		}
		splay(1);
		cout<<(val[1]>1)<<'\n';
	}
	return 0;
}
posted @ 2025-04-22 12:27  TBSF_0207  阅读(166)  评论(0)    收藏  举报