Perface
- 一直没有接触过这个东东,学了一个早上,终于似懂非懂了…
动态树问题
-
一类经典的数据结构题.
-
其实也就是让你动态的维护一些树上的信息,要求在静态的基础上,可以加入这些最基本的操作:
- 加边
- 删边
- 路径修改、询问
- 求LCA,最值,和……
例题 Luogu P3690
- 这个应该算是最简单的动态树问题.
Link Cut Tree 的引入
-
我们需要一种数据结构去解决这类动态树问题.
-
它的名称叫做Link Cut Tree.
与 树链剖分 类比
-
树链剖分用重儿子的定义,把树剖成 条重链.
-
在LCT中,也有类似的定义,称之为偏爱儿子(Preferred Child).
Access
-
在了解偏爱儿子之前,我们首先需要明白Access操作.
-
它的定义是对一个节点进行访问操作,写成
-
这也是LCT的核心操作,一切之基础.
偏爱儿子
-
这个的定义是,在点 (为 的儿子)的子树内,拥有最后一个被访问的节点,那么 就是 的偏爱儿子
-
需要注意,如果进行了 后, 是没有偏爱儿子的.
偏爱链
- 由偏爱儿子相连的链称之为偏爱链(Preferred Path).
LCT的本质
-
LCT实质上就是一些偏爱链构成的.
-
维护这些偏爱链,我们采用splay,理由是势能分析的需要.
-
一颗 就维护一条重链.
-
所以可以看成 .
Splay的维护
-
维护的一条重链,每个节点关键字就是它在原森林中的深度.
-
i.e.这棵 中一个节点的左子树所有结点比当前结点在森林中的深度小,右子树的所有结点比当前结点在森林中的深度大.
再探Access
-
那么如何实现对于一个点的 操作?
-
很简单:
void Access(int x) {
for(int y = 0; x ; y = x, x = fa[x])
splay(x), S[x][1] = y, pushup(x);
}
-
理由是,这个 要被访问,如果它旋转到所在 的根之后,依然有深度比它大的点(即 ),那么显然是需要删掉的,所以一开始 .
-
因为要把 所有点的偏爱儿子都更改,所以我们需要一直往上跳.
-
不妨假设 所在 的根的父亲为 ,那么显然,有 .
-
以此类推。
需要注意
-
会发现,上面的过程中,没有任何更改 的迹象.
-
这是因为,在 中, 的定义比较不同.
-
如果它是它所在 的根节点,那它必然在 里没有父亲,所以就连向它在森林中的父亲,也就成为了它父亲的非偏爱边的儿子.
-
否则就直接向所在 的父亲节点连.
懂得了Access之后
-
最重要的一个操作, ,表示把 变为所在 的根.
-
虽然在 大佬的论文中并没有提到这一操作,但毕竟这种打法还是很简洁的。
-
我们只需要一个 ,便可以很方便的解决 等操作.
-
这个操作是这样的:
- 首先 .
- 然后 .
- 最后 (这个操作的含义是把 的 给翻转一下)
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中的深度,当然是相对深度.
-
所以只需在查询的时候把一个界线点 一下,然后查询点 后再 一下.
-
就是对应深度了.
#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,g,查询是 的。
-
事实证明,比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);
}
}