LCT 学习笔记

简介

构建

类似重链剖分,把原树分为若干条实链,用若干条虚边连接,则我们可以发现所有实链的深度单调递增,考虑每条实链建以深度为键值的 Splay,然后对于虚边实行认父不认子,也就是每颗 Splay 的根有父亲,但它的父亲的儿子没有它。

三个常用函数

getlr(u)

判断 \(u\)\(u\) 父亲的左儿子还是右儿子。

bool getlr(int u){
	return (ch[fa[u]][1]==u);
}

isroot(u)

判断 \(u\) 是不是 \(u\) 所在 Splay 的根,可以利用虚边认父不认子的性质。

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

up(u)

通过 \(u\) 的左右儿子更新 \(u\) 的信息。

两个 Splay 的函数

rotate(u)

\(u\) 进行左旋或右旋,令 \(f = fa[u], g = fa[f]\),特别注意如果 \(g\) 在令一棵 \(Splay\),那么 \(g\) 不能将 \(u\) 作为自己的儿子,其它都和常规 rotate 一样,可以对照代码理解。

void rotate(int u){
	int f = fa[u], g = fa[f], o = getlr(u);
	if (!isroot(f)) ch[g][getlr(f)] = u; 
	fa[u] = g;
	ch[f][o] = ch[u][!o];
	if (ch[u][!o]) fa[ch[u][!o]] = f;
	ch[u][!o] = f; fa[f] = u;
	up(f),up(u);
}

splay(u)

\(u\) 提到当前 Splay 的根,与常规 Splay 大体相同

void splay(int u){
	update(u); // 这个函数后面会讲到
	while (!isroot(u)){
		int f = fa[u], g = fa[f];
		if (!isroot(f)){
			(getlr(u)==getlr(f) ? rotate(f) : rotate(u));
		}
		rotate(u);
	}
}

两个 LCT 核心函数

access(u)

作用是建立一条从 \(u\) 在原树上的根到 \(u\) 的实链,先看代码

void access(int u){
	for (int p=0; u; p = u,u = fa[u]){
		splay(u); ch[u][1] = p; up(u);
	}
}

首先把 \(u\) 旋转到 \(u\) 所在 Splay 的顶部,然后我们需要把 \(u \rightarrow fa[u]\) 的边改为实边,但是注意我们也需要让 \(u\) 和它一个儿子的边变成虚边,

由于我们需要 \(u\)\(u\) 所在原树的根的路径,令 \(p\) 为上一次的 \(u\),当 \(p = 0\) 时,需要断开 \(u\)\(u\) 右子树的边(右子树的点的深度大于 \(u\),没用);当 \(p \ne 0\) 时,那么把抛弃 \(u\) 原有的右儿子,把 \(u\) 的右儿子设成上一个 \(u\) 即可

makeroot(u)

作用是把 \(u\) 设为原树的根,先看代码

void makeroot(int u){
	access(u); splay(u); tag[u] ^= 1;
}

首先,建立一条从 \(u\) 在原树上的根到 \(u\) 的实链,注意到此时 \(u\)\(u\) 所在 Splay 深度最大的店,那么当我们再把 \(u\) 旋到根后,\(u\) 在中序遍历后为最后一名,那么我们把整个中序遍历反过来,不久让 \(u\) 成根了?那么直接套用文艺平衡树的方法,打 tag 即可

这里也对之前的 \(update(u)\) 函数解释,\(Splay(u)\) 前,先把 \(u\) 到根上的点 \(pushdown\)

void update(int u){
	if (!isroot(u)) update(fa[u]);
	down(u);
}

其它操作函数

findroot(u)

找到 \(u\) 所在原树的根

int findroot(int u){
	access(u);
	splay(u);
	while (ls(u)) down(u), u = ls(u);
	splay(u); // 平衡复杂度
	return u;
}

首先 \(access(u)\),把 \(u\) 旋到根,此时我们直接找到最左的儿子就是根,注意要 \(down\),最后还要 \(splay(u)\) 来平衡复杂度(这步后面也有用)

link(u,v)

连边 \((u,v)\)

void link(int u,int v){
	makeroot(u);
	if (findroot(v) != u) fa[u] = v;
}

首先让 \(u\) 为原树的根,然后直接连 \(u \rightarrow v\) 的虚边

cut(u,v)

删边 \((u,v)\),不保证 \((u,v)\) 存在

void cut(int u,int v){
	makeroot(u);
	if (findroot(v)==u && fa[v]==u && !ls(v)){
		rs(u) = fa[v] = 0; 
		up(u);
	}
}

依旧先让 \(u\) 为原树的根,首先判断 \(u\)\(v\) 连不连通

分两种情况

  • \(v\)\(u\) 在同一个 Splay,\(findroot(v)\) 完我们发现此时原树的根 \(u\) 为 Splay 的根,这意味着 \(u\) 无左儿子,那么如果存在 \((u,v)\) 的边,\(v\) 的深度一定是 \(2\),即 \(u\) 的右儿子一定是 \(v\),且 \(v\) 没左儿子
  • \(v\)\(u\) 不在同一个 Splay,\(v\) 的深度也一定是 \(2\)\(findroot(v)\) 完我们发现此时若存在边 \((u,v)\)\(v\) 必须为其所在 Splay 的根且 \(v\) 无左儿子

综上可得到上面精简的判断条件

split(x,y)

整理出 \(y \rightarrow x\) 的路径

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

首先让 \(u\) 为根,然后建一条 \(u \rightarrow v\) 的实链,此时已经可以计算答案,因为 \(u \rightarrow v\) 的点都在这个 Splay 里了,但是为了平衡复杂度还是把 \(v\) 旋到根,取 \(v\) 的信息

题目

维护树链信息

P2486 [SDOI2011] 染色

树剖+线段树可以直接过,但是有单 \(\log\) 的 LCT 做法

考虑 \(up(u)\) 函数怎么实现,根据 BST 的性质,相当于 \(u\) 子树内路径的颜色 = \(u\) 左儿子子树内路径的颜色 + \(u\) 的颜色 + \(u\) 右儿子子树内路径的颜色,类似线段树区间合并,维护 \(lc[u],rc[uj\) 表示 \(u\) 子树内路径最左端的颜色和最右端的颜色,然后合并信息

void up(int u){
	int res = val[ls(u)]+val[rs(u)]+1;
	if (ls(u) && rc[ls(u)] == ar[u]) res--;
	if (rs(u) && ar[u] == lc[rs(u)]) res--;
	val[u] = res;
	lc[u] = (ls(u)?lc[ls(u)]:ar[u]);
	rc[u] = (rs(u)?rc[rs(u)]:ar[u]);
}

特别需要注意的是,若对 \(u\) 进行子树翻转操作,那么 \(lc[u]\)\(rc[u]\) 也需 \(swap\)

维护子树信息

LCT 其实并不好维护子树信息,需要满足子树信息有可减性。

我们多维护 \(u\) 的所有虚子节点的信息,注意在一些地方更新

access

void access(int u){
	for (int p=0; u; p = u,u = fa[u]){
		splay(u);
		// 更新信息
		rs(u) = p;
		up(u);
	}
}

link(注意注释内容)

void link(int u,int v){
	makeroot(u);
	makeroot(v); // 由于需要更新, 如果没有这步,需要把整颗树都更新,十分不方便,有了这步就只用更新 v 
	if (findroot(v) != u){
		fa[u] = v;
		// 更新信息
	}
}

P4219 [BJOI2014] 大融合

询问时,先断 \((x,y)\),然后变成 \(x\)\(y\) 的子树和,维护即可

posted @ 2025-11-09 21:37  huangyuze  阅读(1)  评论(0)    收藏  举报