最近正是实验课的高峰期,我数了一下,除了毛概没有实验课,其他的课都有实验课。。。不过好在这些实验都不是很难。我尽力挤出时间用来刷题。
简介
Link/Cut Tree和树链剖分很相似,二者处理的问题也有重叠。区别在于后者用线段树维护树链,所以树链是静态的,剖分方式是重链剖分;后者是用Splay维护树链,是动态的,剖分方式是实链剖分,所以Link/Cut Tree有时也被称为动态树(它只是动态树的一种)。
树链剖分的剖分方式结果是由子树决定的,即由题目中给出的数据决定的;而Link/Cut Tree的剖分方式是我们自己选择的,不受题目的限制,且受益于Splay的灵活性,我们可以随意改动树的结构。以此实现树链的 l i n k link link 和 c u t cut cut 操作。
Link/Cut Tree维护的是一个森林,每棵树是一个Splay树,Splay树内的边为实边,树与树之间有一些虚边连接。实边就是双向边,即儿子节点指向父亲节点,父亲节点也指向儿子节点,而虚边是单向边,儿子节点指向父亲节点,父亲节点不指向儿子节点。
性质
- 每一个Splay的中序遍历序列的点,对应在原树中的深度严格递增。
- 每个节点被包含且仅被包含于一个Splay中。
- 因为性质2,当某点在原树中有多个儿子时,只能向其中一个儿子拉一条实链(只认一个儿子),而其它儿子是不能在这个Splay中的。那么为了保持树的形状,我们要让到其它儿子的边变为虚边,由对应儿子所属的Splay的根节点的父亲指向该点,而从该点并不能直接访问该儿子(认父不认子)。
有一些容易混淆的概念。
原树是指题目给出的树,我们在原树里按照上述性质划分各个实边和虚边。
划分完成后,由实边组成的联通块组成一个Splay,每一个Splay的结构与他们在原树中的位置无关,Splay的结构只受限于“每一个Splay的中序遍历序列的点,对应在原树中的深度严格递增”。
结构
#define lc T[x][0]
#define rc T[x][1]
ch[N][2]:左右儿子
f[N]:父亲指向
tag[N]:翻转标记
laz[N]:权值标记
siz[N]:辅助树上子树大小
基本操作
PushUp
更新一下子树的大小,在节点位置发生变化时调用。
inline void pushup(register int x) {
//上传信息
siz[x] = siz[lc] + siz[rc];
}
PushDown
释放懒标记,向下移动时调用。
inline void pushr(register int x) {
swap(lc, rc);
tag[x] ^= 1;
} //翻转操作
inline void pushdown(register int x) {
//判断并释放懒标记
if (tag[x]) {
if (lc) pushr(lc);
if (rc) pushr(rc);
tag[x] = 0;
}
}
isRoot
判断节点是否为一个Splay的根(与普通Splay的区别1)。
是返回1,否则返回0.
inline bool nroot(register int x) {
//判断节点是否为一个Splay的根(与普通Splay的区别1)
return T[f[x]][0] == x || T[f[x]][1] == x;
} //原理很简单,如果连的是轻边,他的父亲的儿子里没有它
Splay && Rotate
Splay中的基础操作。
Splay()只有一个参数,表示将 x x x 移动到根节点。
inline void rotate(register int x) {
//一次旋转
register int y = f[x], z = f[y], k = T[y][1] == x, w = T[x][!k];
if (nroot(y)) T[z][T[z][1] == y] = x;
T[x][!k] = y;
T[y][k] = w; //额外注意if(nroot(y))语句,此处不判断会引起致命错误(与普通Splay的区别2)
if (w) f[w] = y;
f[y] = x;
f[x] = z;
pushup(y);
}
inline void splay(
register int x) {
//只传了一个参数,因为所有操作的目标都是该Splay的根(与普通Splay的区别3)
register int y = x, z = 0;
st[++z] = y; // st为栈,暂存当前点到根的整条路径,pushdown时一定要从上往下放标记(与普通Splay的区别4)
while (nroot(y)) st[++z] = y = f[y];
while (z) pushdown(st[z--]);
while (nroot(x)) {
y = f[x];
z = f[y];
if (nroot(y)) rotate((T[y][0] == x) ^ (T[z][0] == y) ? x : y);
rotate(x);
}
pushup(x);
}
Access
Link/Cut Tree核心操作,将根节点到 x x x 的路径点划分到一个Splay中。
- 把当前节点转到根。
- 把儿子换成之前的节点。
- 更新当前点的信息。
- 把当前点换成当前点的父亲,继续操作。
我们有这样一棵树,实线为实边,虚线为虚边。
它的辅助树可能长成这样(构图方式不同可能 LCT 的结构也不同)。
每个绿框里是一棵 Splay。
现在我们要 A c c e s s ( N ) Access(N) Access(N) , 把 A A A 到 N N N 路径上的边都变为实边,拉成一棵 Splay。
实现的方法是从下到上逐步更新 Splay。
首先我们要把 N N N 旋至当前 Splay 的根。
为了保证 AuxTree(辅助树)的性质,原来 N N N 到 O O O 的实边要更改为虚边。
由于认父不认子的性质,我们可以单方面的把 N N N 的儿子改为 Null。
于是原来的 AuxTree 就从下图变成了下下图。
下一步,我们把 N N N 指向的 F a [ I ] Fa[I] Fa[I] 也旋转到 I I I 的 Splay 树根。
原来的实边 I − K I - K I−K 要去掉,这时候我们把 I I I 的右儿子指向 N N N , 就得到了 I − L I - L I−L 这样一棵 Splay。
接下来,按照刚刚的操作步骤,由于 I I I 的 F a [ H ] Fa[H] Fa[H] , 我们把 H H H 旋转到他所在 Splay Tree 的根,然后把 H H H 的 r s rs rs 设为 I I I 。
之后的树是这样的。
同理我们 Splay(A) , 并把 A A A 的右儿子指向 H H H 。
于是我们得到了这样一棵 AuxTree。并且发现 A − H A - H A−H 的整个路径已经在同一棵 Splay 中了。大功告成!
inline void access(register int x) {
//访问
for (register int y = 0; x; x = f[y = x]) splay(x), rc = y, pushup(x);
}
makeRoot
把节点 x x x 成为原树的根节点。
inline void makeroot(register int x) {
//换根
access(x);
splay(x);
pushr(x);
}
FindRoot
找到 x x x 在原树中的根,判断两点连通性的时候会用到。
先 a c c e s s ( x ) access(x) access(x),那么x就和根节点在同一Splay中了。然后 s p l a y ( x ) splay(x) splay(x),那么 x x x 就成了Splay的根(Splay的根和原树的根不是一个概念)。
由于Splay符合二叉搜索树的性质,而且根节点的深度一定最小,所以只需要从 x x x 以置向左走,就能最后走到原树的根节点。
最后将找到的原树根节点 s p l a y splay splay 一下,以防被卡。
int findroot(register int x) {
//找根(在真实的树中的)
access(x);
splay(x);
while (lc) pushdown(x), x = lc;
splay(x);
return x;
}
Link
连一条 x − y x - y x−y 的边。此处使 x x x 的父亲指向 y。
连边之前需要判两点是否已经联通。先把 x x x 置为原树的根节点,如果 x x x 和 y y y 已经联通那么在 y y y 的位置查询到的根节点一定就是 x x x ,否则 x x x 和 y y y 未联通。
我们在这里虽然改变了原树的根节点,但是原树的结构是没有发生变化的,这和Splay的 s p l a y splay splay 操作不同。
inline void link(register int x, register int y) {
//连边
makeroot(x);
if (findroot(y) != x) f[x] = y;
}
Split
提取出原树中 x x x 点到 y y y 点的路径。
先把 x x x 点设置为原树的根,然后 a c c e s s ( y ) access(y) access(y) 即可。此时 x x x 为根,即深度最小的节点, y y y 没有右儿子,即 y y y 是当前 Splay 中深度最大的节点。那么中序遍历的序列就是 x x x 开头, y y y 结尾。
最后 s p l a y splay splay 一下,防止被卡。
inline void split(register int x, register int y) {
//提取路径
makeroot(x);
access(y);
splay(y);
}
Cut
我认为对初学者来说这个有点难以理解。
首先需要检查 x x x 和 y y y 是否已经联通。如果联通了且两点相邻,则断开这条边;否则两点不联通,不用执行任何操作。
先把 x x x 置为原树的根结点,然后查询 y y y 的根结点是否为 x x x 。是的话则 x x x 和 y y y 此时在同一个Splay,即两点在原树中已经联通。
如果 x x x 和 y y y 联通,那么需要判断 x x x 和 y y y 在原树中是否相邻。由于前面把 x x x 置为原树的根结点,所以此时 x x x 的深度最小(没有左儿子),那么 y y y 一定在 x x x 的右儿子的子树里。
那么如果 x x x 和 y y y 在原树中相邻,中序遍历这颗Splay后, x x x 和 y y y 在序列里的位置一定相邻(中序遍历一颗Splay,得到的序列的点在原树中的深度递增,所以路径在原树中是一颗向下的链,且没有分支)。那么 y y y 的父亲节点一定是 x x x 且 y y y 没有左儿子。如果 y y y 有左儿子,那么中序遍历时访问 x x x 后会先访问 y y y 的左儿子, x x x 和 y y y 在序列里不相邻。
断边时双向断边。
inline void cut(register int x, register int y) {
//断边
makeroot(x);
if (findroot(y) == x && f[y] == x && !T[y][0]) {
f[y] = T[x][1] = 0;
pushup(x);
}
}
模板
#define lc T[x][0]
#define rc T[x][1]
int f[maxn], T[maxn][2], siz[maxn], st[maxn];
bool tag[maxn];
inline bool nroot(register int x) {
//判断节点是否为一个Splay的根(与普通Splay的区别1)
return T[f[x]][0] == x || T[f[x]][1] == x;
} //原理很简单,如果连的是轻边,他的父亲的儿子里没有它
inline void pushup(register int x) {
//上传信息
siz[x] = siz[lc] + siz[rc];
}
inline void pushr(register int x) {
swap(lc, rc);
tag[x] ^= 1;
} //翻转操作
inline void pushdown(register int x) {
//判断并释放懒标记
if (tag[x]) {
if (lc) pushr(lc);
if (rc) pushr(rc);
tag[x] = 0;
}
}
inline void rotate(register int x) {
//一次旋转
register int y = f[x], z = f[y], k = T[y][1] == x, w = T[x][!k];
if (nroot(y)) T[z][T[z][1] == y] = x;
T[x][!k] = y;
T[y][k] = w; //额外注意if(nroot(y))语句,此处不判断会引起致命错误(与普通Splay的区别2)
if (w) f[w] = y;
f[y] = x;
f[x] = z;
pushup(y);
}
inline void splay(
register int x) {
//只传了一个参数,因为所有操作的目标都是该Splay的根(与普通Splay的区别3)
register int y = x, z = 0;
st[++z] = y; // st为栈,暂存当前点到根的整条路径,pushdown时一定要从上往下放标记(与普通Splay的区别4)
while (nroot(y)) st[++z] = y = f[y];
while (z) pushdown(st[z--]);
while (nroot(x)) {
y = f[x];
z = f[y];
if (nroot(y)) rotate((T[y][0] == x) ^ (T[z][0] == y) ? x : y);
rotate(x);
}
pushup(x);
}
/*当然了,其实利用函数堆栈也很方便,代替上面的手工栈,就像这样
inline void pushall(register int x){
if(nroot(x))pushall(f[x]);
pushdown(x);
}*/
inline void access(register int x) {
//访问
for (register int y = 0; x; x = f[y = x]) splay(x), rc = y, pushup(x);
}
inline void makeroot(register int x) {
//换根
access(x);
splay(x);
pushr(x);
}
int findroot(register int x) {
//找根(在真实的树中的)
access(x);
splay(x);
while (lc) pushdown(x), x = lc;
splay(x);
return x;
}
inline void split(register int x, register int y) {
//提取路径
makeroot(x);
access(y);
splay(y);
}
inline void link(register int x, register int y) {
//连边
makeroot(x);
if (findroot(y) != x) f[x] = y;
}
inline void cut(register int x, register int y) {
//断边
makeroot(x);
if (findroot(y) == x && f[y] == x && !T[y][0]) {
f[y] = T[x][1] = 0;
pushup(x);
}
}
模板题
代码
// #pragma GCC optimize(2)
#include <bits/stdc++.h>
#define m_p make_pair
#define p_i pair<int, int>
#define _for(i, a) for(register int i = 0, lennn = (a); i < lennn; ++i)
#define _rep(i, a, b) for(register int i = (a), lennn = (b); i <= lennn; ++i)
#define outval(a) cout << "Debuging...|" << #a << ": " << a << "\n"
#define mem(a, b) memset(a, b, sizeof(a))
#define mem0(a) memset(a, 0, sizeof(a))
#define fil(a, b) fill(a.begin(), a.end(), b);
#define scl(x) scanf("%lld", &x)
#define sc(x) scanf("%d", &x)
#define pf(x) printf("%d\n", x)
#define pfl(x) printf("%lld\n", x)
#define abs(x) ((x) > 0 ? (x) : -(x))
#define PI acos(-1)
#define lowbit(x) (x & (-x))
#define dg if(debug)
#define nl(i, n) (i == n - 1 ? "\n":" ")
using namespace std;
typedef long long LL;
// typedef __int128 LL;
typedef unsigned long long ULL;
const int maxn = 100005;
const int maxm = 1000005;
const int maxp = 30;
const int inf = 0x3f3f3f3f;
const LL INF = 0x3f3f3f3f3f3f3f3f;
const int mod = 1000000007;
const double eps = 1e-8;
const double e = 2.718281828;
int debug = 0;
inline int read() {
int x(0), f(1); char ch(getchar());
while (ch<'0' || ch>'9') {
if (ch == '-') f = -1; ch = getchar(); }
while (ch >= '0'&&ch <= '9') {
x = x * 10 + ch - '0'; ch = getchar(); }
return x * f;
}
int v[maxn];
#define lc T[x][0]
#define rc T[x][1]
int f[maxn], T[maxn][2], val[maxn], st[maxn];
bool tag[maxn];
inline bool nroot(register int x) {
//判断节点是否为一个Splay的根(与普通Splay的区别1)
return T[f[x]][0] == x || T[f[x]][1] == x;
} //原理很简单,如果连的是轻边,他的父亲的儿子里没有它
inline void pushup(register int x) {
//上传信息
val[x] = val[lc] ^ val[rc] ^ v[x];
}
inline void pushr(register int x) {
swap(lc, rc);
tag[x] ^= 1;
} //翻转操作
inline void pushdown(register int x) {
//判断并释放懒标记
if (tag[x]) {
if (lc) pushr(lc);
if (rc) pushr(rc);
tag[x] = 0;
}
}
inline void rotate(register int x) {
//一次旋转
register int y = f[x], z = f[y], k = T[y][1] == x, w = T[x][!k];
if (nroot(y)) T[z][T[z][1] == y] = x;
T[x][!k] = y;
// pushup(x);
T[y][k] = w; //额外注意if(nroot(y))语句,此处不判断会引起致命错误(与普通Splay的区别2)
if (w) f[w] = y;
f[y] = x;
f[x] = z;
pushup(y);
}
inline void splay(register int x) {
register int y = x, z = 0;
st[++z] = y; // st为栈,暂存当前点到根的整条路径,pushdown时一定要从上往下放标记(与普通Splay的区别4)
while (nroot(y)) st[++z] = y = f[y];
while (z) pushdown(st[z--]);
while (nroot(x)) {
y = f[x];
z = f[y];
if (nroot(y)) rotate((T[y][0] == x) ^ (T[z][0] == y) ? x : y);
rotate(x);
}
pushup(x);
}
/*当然了,其实利用函数堆栈也很方便,代替上面的手工栈,就像这样
inline void pushall(register int x){
if(nroot(x))pushall(f[x]);
pushdown(x);
}*/
inline void access(register int x) {
//访问
for (register int y = 0; x; x = f[y = x]) splay(x), rc = y, pushup(x);
}
inline void makeroot(register int x) {
//换根
access(x);
splay(x);
pushr(x);
}
int findroot(register int x) {
//找根(在真实的树中的)
access(x);
splay(x);
while (lc) pushdown(x), x = lc;
splay(x);
return x;
}
inline void split(register int x, register int y) {
//提取路径
makeroot(x);
access(y);
splay(y);
}
inline void link(register int x, register int y) {
//连边
makeroot(x);
if (findroot(y) != x) f[x] = y;
}
inline void cut(register int x, register int y) {
//断边
makeroot(x);
if (findroot(y) == x && f[y] == x && !T[y][0]) {
f[y] = T[x][1] = 0;
pushup(x);
}
}
int main() {
//ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
#ifdef ONLINE_JUDGE
#else
freopen("in.txt", "r", stdin);
debug = 1;
#endif
time_t beg, end;
if(debug) beg = clock();
int n = read(), m = read();
_rep(i, 1, n) v[i] = read();
_for(i, m) {
int op = read(), x = read(), y = read();
if(op == 0) split(x, y), printf("%d\n", val[y]);
else if(op == 1) link(x, y);
else if(op == 2) cut(x, y);
else if(op == 3) v[x] = y, splay(x);
}
if(debug) {
end = clock();
printf("time:%.2fs\n", 1.0 * (end - beg) / CLOCKS_PER_SEC);
}
return 0;
}
参考:
OI Wiki
博客园-FlashHu