初探 Link Cut Tree

版权声明:蒟蒻写的文章,能看就行了,同时欢迎大佬们指点错误 https://blog.csdn.net/Algor_pro_king_John/article/details/84197324
Perface
  • 一直没有接触过这个东东,学了一个早上,终于似懂非懂了…
动态树问题
  • 一类经典的数据结构题.

  • 其实也就是让你动态的维护一些树上的信息,要求在静态的基础上,可以加入这些最基本的操作:

    • 加边
    • 删边
    • 路径修改、询问
    • 求LCA,最值,和……
例题 Luogu P3690
  • 这个应该算是最简单的动态树问题.
Link Cut Tree 的引入
  • 我们需要一种数据结构去解决这类动态树问题.

  • 它的名称叫做Link Cut Tree.

与 树链剖分 类比
  • 树链剖分用重儿子的定义,把树剖成 l o g log 条重链.

  • 在LCT中,也有类似的定义,称之为偏爱儿子(Preferred Child).

Access
  • 在了解偏爱儿子之前,我们首先需要明白Access操作.

  • 它的定义是对一个节点进行访问操作,写成 A c c e s s ( x ) Access(x)

  • 这也是LCT的核心操作,一切之基础.

偏爱儿子
  • 这个的定义是,在点 y y (为 x x 的儿子)的子树内,拥有最后一个被访问的节点,那么 y y 就是 x x 偏爱儿子

  • 需要注意,如果进行了 A c c e s s ( x ) Access(x) 后, x x 没有偏爱儿子的.

偏爱链
  • 偏爱儿子相连的链称之为偏爱链(Preferred Path).
LCT的本质
  • LCT实质上就是一些偏爱链构成的.

  • 维护这些偏爱链,我们采用splay,理由是势能分析的需要.

  • 一颗 s p l a y splay 就维护一条重链.

  • 所以可以看成 L C T = + s p l a y LCT = 链剖分 + splay .

Splay的维护
  • S p l a y Splay 维护的一条重链,每个节点关键字就是它在原森林中的深度.

  • i.e.这棵 S p l a y Splay 中一个节点的左子树所有结点比当前结点在森林中的深度小,右子树的所有结点比当前结点在森林中的深度大.

再探Access
  • 那么如何实现对于一个点的 A c c e s s Access 操作?

  • 很简单:

void Access(int x) {
    for(int y = 0; x ; y = x, x = fa[x])
		splay(x), S[x][1] = y, pushup(x);
}
  • 理由是,这个 x x 要被访问,如果它旋转到所在 s p l a y splay 的根之后,依然有深度比它大的点(即 S [ x ] [ 1 ] S[x][1] ),那么显然是需要删掉的,所以一开始 y = 0 y=0 .

  • 因为要把 x R o o t x\rightarrow Root 所有点的偏爱儿子都更改,所以我们需要一直往上跳.

  • 不妨假设 x x 所在 s p l a y splay 的根的父亲为 y y ,那么显然,有 S o n [ x ] [ 1 ] = y Son[x][1]=y .

  • 以此类推。

需要注意
  • 会发现,上面的过程中,没有任何更改 f a fa 的迹象.

  • 这是因为,在 L C T LCT 中, f a fa 的定义比较不同.

  • 如果它是它所在 s p l a y splay 的根节点,那它必然在 s p l a y splay 里没有父亲,所以就连向它在森林中的父亲,也就成为了它父亲的非偏爱边的儿子.

  • 否则就直接向所在 s p l a y splay 的父亲节点连.

懂得了Access之后
  • 最重要的一个操作, M a k e R o o t ( x ) MakeRoot(x) ,表示把 x x 变为所在 L C T LCT 的根.

  • 虽然在 Y a n g Z h e YangZhe 大佬的论文中并没有提到这一操作,但毕竟这种打法还是很简洁的。

  • 我们只需要一个 M a k e R o o t ( x ) MakeRoot(x) ,便可以很方便的解决 l i n k , c u t link, cut 等操作.

  • 这个操作是这样的:

    • 首先 A c c e s s ( x ) Access(x) .
    • 然后 S p l a y ( x ) Splay(x) .
    • 最后 R e v e r s e ( x ) Reverse(x) (这个操作的含义是把 x x s p l a y splay 给翻转一下)
Code
#include<cstdio>
#include<set>

#define F(i, a, b) for (int i = a; i <= b; i ++)

using namespace std;

const int N = 300001;

set <int> Link[N];
int n, m, x, y, type;
int fa[N], S[N][2], v[N], sum[N], rev[N], val[N];

void pushup(int x) {
	sum[x] = sum[S[x][0]] ^ sum[S[x][1]] ^ v[x]; //求异或和 
}
void reverse(int x) {
	swap(S[x][0], S[x][1]);
	rev[x] ^= 1; //标记rev[x]=1表示已经翻转了x的左右儿子 
}
void pushdown(int x) { //向下传递翻转标记 
    if (!rev[x]) return;
    reverse(S[x][0]), reverse(S[x][1]);
	rev[x] = 0;
}
bool SON(int x) { return S[fa[x]][1] == x; }
bool can(int x) { return S[fa[x]][SON(x)] == x; }
// 返回x是否为其所在splay的根,不是就返回true 

void rotate(int x) {
    int y = fa[x], z = fa[y], k = SON(x);
    if(can(y)) S[z][SON(y)] = x;
    fa[x] = z, S[y][k] = S[x][k ^ 1]; fa[S[x][k ^ 1]] = y;
    S[x][k ^ 1] = y; fa[y] = x;
    pushup(y);
} //最普通的rotate

void Go(int x) {
	if (can(x)) Go(fa[x]);
	pushdown(x);
} //从上往下下传翻转标记

void splay(int x) {
	Go(x);
	for (int y; can(x); rotate(x))
		if (can(y = fa[x])) rotate((SON(y) == SON(x)) ? x : y);
    pushup(x);
} //最普通的splay

void Access(int x) {
    for(int y = 0; x ; y = x, x = fa[x])
		splay(x), S[x][1] = y, pushup(x);
} // 核心操作

void makeroot(int x) {
    Access(x); splay(x);
	reverse(x);
} // 表示使x成为LCT的根.

int Findroot(int x) {
    Access(x); splay(x);
    while (S[x][0]) x = S[x][0];
    return x;
} // 求出x所在LCT的根.

void split(int x,int y) { makeroot(x), Access(y); splay(y); }
// 分离x->y这条路径出来,注意splay(y),所以之后y才是询问的答案.

void link(int x,int y) {
    makeroot(x), fa[x] = y; //连边x<->y
    Link[x].insert(y), Link[y].insert(x);
    //此步乃出题人之无聊,一般来说题目会保证没有重边的。
} 
void cut(int x,int y) {
    split(x, y), S[y][0] = fa[x] = 0; //删边x<->y
    Link[x].erase(y), Link[y].erase(x);
}

int main() {
    scanf("%d %d", &n, &m); 
    F(i, 1, n)
		scanf("%d", &x), sum[i] = v[i] = x;
    F(i, 1, m) {
        scanf("%d%d%d", &type, &x, &y);
        switch (type) {
        	case 0 : split(x, y); printf("%d\n", sum[y]); break;
        	case 1 : if (Findroot(x) ^ Findroot(y)) link(x, y); break;
        	case 2 : if (Link[x].find(y) != Link[x].end()) cut(x, y); break;
        	case 3 : Access(x); splay(x); v[x] = y; pushup(x); break;
		}
    }
}
2002: [Hnoi2010]Bounce 弹飞绵羊
  • 弱智LCT了…

  • 要求一个点在LCT中的深度,当然是相对深度.

  • 所以只需在查询的时候把一个界线点 ( n + 1 ) A c c e s s (n+1) Access 一下,然后查询点 A c c e s s Access 后再 s p l a y splay 一下.

  • S i z e Size 就是对应深度了.

#include <cstdio>
#include <algorithm>

#define F(i, a, b) for (int i = a; i <= b; i ++)
#define min(a, b) ((a) < (b) ? (a) : (b))

const int N = 2e5 + 10;

using namespace std;

int n, m, type, k, v;
int a[N], fa[N], S[N][2], rev[N], Sz[N];

bool SON(int x) { return S[fa[x]][1] == x; }
bool can(int x) { return S[fa[x]][SON(x)] == x; }
void Update(int x) { Sz[x] = Sz[S[x][0]] + Sz[S[x][1]] + 1; }
void Reverse(int x) { swap(S[x][0], S[x][1]), rev[x] ^= 1; }
void PushDown(int x) {
	if (!rev[x]) return;
	Reverse(S[x][0]), Reverse(S[x][1]), rev[x] = 0;
}
void rotate(int x) {
	int y = fa[x], z = fa[y], k = SON(x);
	if (can(y)) S[z][SON(y)] = x;
	fa[x] = z, fa[y] = x, S[y][k] = S[x][1 ^ k], fa[S[x][1 ^ k]] = y, S[x][1 ^ k] = y;
	Update(y);
}
void Go(int x) {
	if (can(x)) Go(fa[x]);
	PushDown(x);
}
void splay(int x) {
	Go(x);
	for (int y; can(x); rotate(x))
		if (can(y = fa[x])) rotate(SON(y) == SON(x) ? x : y);
	Update(x);
}
void Access(int x) {
	for (int y = 0; x ; y = x, x = fa[x])
		splay(x), S[x][1] = y, Update(x);
}
void MakeRt(int x) { Access(x); splay(x); Reverse(x); }
void link(int x, int y) {
	MakeRt(x);
	fa[x] = y;
}
void cut(int x, int y) {
	MakeRt(x);
	Access(y);
	splay(y);
	S[y][0] = fa[x] = 0;
	Update(y);
}

int main() {
	scanf("%d", &n);
	F(i, 1, n)
		scanf("%d", &a[i]), link(i, min(i + a[i], n + 1));
	scanf("%d", &m);
	F(i, 1, m) {
		scanf("%d%d", &type, &k), k ++;
		if (type > 1) {
			scanf("%d", &v);
			cut(k, min(k + a[k], n + 1));
			a[k] = v;
			link(k, min(k + a[k], n + 1));
			continue;
		}
		MakeRt(n + 1);
		Access(k);
		splay(k);
		printf("%d\n", Sz[k] - 1);
	}
}
  • 当然,这题还有一个分块做法,也学习了:

  • f [ i ] f[i] 表示在 i i 这个点跳出块后到达的位置。

  • g [ i ] g[i] 表示对应步数。

  • 修改时,修改一个块的f,g,查询是 O ( n ) O(\sqrt{n}) 的。

  • 事实证明,比LCT快、短、妙

#include <cstdio>
#include <cmath>

using namespace std;

#define F(i, a, b) for (int i = a; i <= b; i ++)
#define G(i, a, b) for (int i = a; i >= b; i --)
#define min(a, b) ((a) < (b) ? (a) : (b))
#define cal(x) (((x - 1) / K + 1) * K)

const int N = 4e5 + 10;

int n, Ans, m, x, y, k, j, K, T;
int a[N], cnt[N], f[N];

void Doit(int x) {
	T = min(n, cal(x));
	if (x + a[x] <= T)
		f[x] = f[x + a[x]], cnt[x] = cnt[x + a[x]] + 1;
	else
		f[x] = x + a[x], cnt[x] = 1;
}

int main() {
	scanf("%d", &n), K = int(sqrt(n));
	F(i, 1, n) scanf("%d", &a[i]);
	G(i, n, 1) Doit(i);
	scanf("%d", &m);
	F(i, 1, m) {
		scanf("%d%d", &x, &y), y ++;
		if (x > 1) {
			scanf("%d", &k), a[y] = k;
			G(t, y, cal(y) - K + 1)
				Doit(t);
			continue;
		}
		for (Ans = 0, j = y; j <= n;)
			Ans += cnt[j], j = f[j];
		printf("%d\n", Ans);
	}
}


猜你喜欢

转载自blog.csdn.net/Algor_pro_king_John/article/details/84197324
今日推荐