动态树模板题两道。
[lydsy2049]洞穴勘测:维护一棵树,支持如下操作:
Connect u v
连接两点 u, v (保证 u, v 不连通)。Destroy u v
删除两点 u, v 之间的边(保证 u, v 连通)。Query u v
询问 u, v 的连通性。[luogu3690]【模板】Link Cut Tree:维护一棵树,每个点有一个对应的权值 wi,支持如下操作:
没什么好说的,见原题。
终于学动态树了!
你知道什么是动态树吗?左图就是一棵动态树。
动态树是维护一棵可以动态加边删边的树,然后在树上搞神马树剖啦,LCA啦,仙人掌嵌套动态网络流维护A*搜索等等。
哦对了,有人问我第一题能不能用并查集骗,其实第一题去掉 Destroy 操作可以用并查集做,说的也没错,可是并查集貌(di)似(que)是不支持动态加删边的。(不过好像这道题用并查集能拿很高的分)
动态树其实有很多种,最(wo)主(zhi)流(hui)的动态树就是 Link-Cut Tree,简称 LCT。
LCT 就是将一棵树先进行树链剖分(没有学过树剖的自行左转小学(没错,就是小学)深造)。
将这棵树剖成几条重重轻轻的链以后,然后用 spaly,哦不,splay 维护每个链的形态。
(scx: 不应该是线段树么?splay 是平衡树,是维护一棵有序序列的!)
线段树是可以用,但是 splay 会有更高的效率,splay 是维护这样一个东西:
假如有一棵重链(显然是单调上升或单调下降的),因此可以唯一地将这个链上的点从浅至深的遍历
那么,我们只需使这个 splay 的每一个节点的左子树存储比它浅的节点,右子树存储比它深的节点,那么将它中序遍历一下,就是刚才所说的从浅至深的遍历。
(scx: 那么我们怎么知道要多少个splay呢?如何保证内存呢?)
其实不难。我们可以见一个主splay,如果一条重链的最顶端不是根,设这个点 V 的父亲为 U,我们知道,这时点 V 在这个 splay 的最左端,而它所在子树的根节点没有父亲,所以我们将那个根节点的父亲设为 U,但它不作为节点 U 的任何子树,我们将这样的边叫做虚边(void edge)。
这样,所有的 splay 并成了一棵树,把它叫做辅助树(auxiliary tree),这棵树所表示的,也就是题目中的原来的树,叫做原树(primary tree),这样,我们把原树的几乎所有信息转移到了辅助树上。
(scx: 那如何在这棵辅助树上动态加(删)边呢?)
我们需要这样几个操作:
void pull_down(int x){ if(~dir(x)) pull_down(x[nd].p); push_down(x); }
这样就能实现一个基本的 splay 了,代码见"代码"部分,如果还有维护神马东西的话,增加一个 Update 函数,更新维护的权值,只需在 rotate、access、cut 时增加一句 update 即可。
两道题还有一个区别,就是第一题的 link, cut 有连通性保证,第二题没有,所以代码也会有区别(本例中改为了 Trylink, Trycut 函数)。
毕竟这两道题是动态树模板题,所以其它也没什么好讲的了。
#include <bits/stdc++.h>
#define LEFT 0
#define RIGHT 1
#define pa p[nd]
#define root nd[0].c[0]
#define N 10034
using namespace std;
struct node{
int v, rev;
int c[2], p;
}nd[N];
// get direction of x, root: -1, left: 0, right: 1
inline int dir(int x){return !x[nd].p ? -1 : (x == x[nd].pa.c[LEFT] ? 0 : (x == x[nd].pa.c[RIGHT] ? 1 : -1));}
// reverse the subtree of x
void reverse(int x){swap(x[nd].c[LEFT], x[nd].c[RIGHT]); x[nd].rev ^= 1;}
// push_down the tags
void push_down(int x){if(x[nd].rev){reverse(x[nd].c[LEFT]); reverse(x[nd].c[RIGHT]);} x[nd].rev = 0;}
// push_down all the tags over x
void pull_down(int x){if(~dir(x)) pull_down(x[nd].p); push_down(x);}
// rotate node x
void rotate(int x){
int y = x[nd].p, d = !dir(x);
nd[y[nd].c[!d] = x[nd].c[d]].p = y; // change the subtree
x[nd].p = y[nd].p; // change the parent of parent
if(~dir(y)) y[nd].pa.c[dir(y)] = x;
nd[x[nd].c[d] = y].p = x; // change itself and parent
}
// splay x to root
void splay(int x){for(pull_down(x); ~dir(x); rotate(x))
if(~dir(x[nd].p)) rotate(dir(x) ^ dir(x[nd].p) ? x : x[nd].p);}
// turn all the edge from x into root into preferred edge
void access(int x){for(int y = 0; x; y = x, x = x[nd].p){splay(x); x[nd].c[RIGHT] = y;}}
// make x to root
void make_root(int x){access(x); splay(x); reverse(x);}
// link edge {x, y}
void link(int x, int y){make_root(x); x[nd].p = y;}
// cut edge {x, y}
void cut(int x, int y){make_root(x); access(y); splay(y); y[nd].c[LEFT] = x[nd].p = 0;}
// find the root of temp tree of x
int find_root(int x){access(x); splay(x); for(; x[nd].c[LEFT]; x = x[nd].c[LEFT]); return x;}
int V, Q;
int u, v, ur, vr;
char op[16];
int main(){
scanf("%d%d", &V, &Q);
for(; Q; Q--){
scanf("%s%d%d", op, &u, &v);
if(op[0] == 'C') link(u, v);
else if(op[0] == 'D') cut(u, v);
else if(op[0] == 'Q'){
ur = find_root(u); vr = find_root(v);
puts(ur == vr ? "Yes" : "No");
}
}
}
#include <bits/stdc++.h>
#define LEFT 0
#define RIGHT 1
#define pa p[nd]
#define root nd[0].c[0]
#define N 300034
using namespace std;
struct node{
int v, rev;
int c[2], p;
}nd[N];
int a[N];
int dir(int x){
return !x[nd].p ? -1 : (x == x[nd].pa.c[LEFT] ? 0 : (x == x[nd].pa.c[RIGHT] ? 1 : -1));
}
void reverse(int x){
swap(x[nd].c[LEFT], x[nd].c[RIGHT]);
x[nd].rev ^= 1;
}
void push_down(int x){
if(x[nd].rev){reverse(x[nd].c[LEFT]); reverse(x[nd].c[RIGHT]);}
x[nd].rev = 0;
}
void pull_down(int x){
if(~dir(x)) pull_down(x[nd].p);
push_down(x);
}
void update(int x){
x[nd].v = x[nd].c[LEFT][nd].v ^ x[nd].c[RIGHT][nd].v ^ a[x];
}
void rotate(int x){
int y = x[nd].p, d = !dir(x);
nd[y[nd].c[!d] = x[nd].c[d]].p = y;
x[nd].p = y[nd].p;
if(~dir(y)) y[nd].pa.c[dir(y)] = x;
nd[x[nd].c[d] = y].p = x;
update(y); update(x);
}
void splay(int x){
for(pull_down(x); ~dir(x); rotate(x))
if(~dir(x[nd].p))
rotate(dir(x) ^ dir(x[nd].p) ? x : x[nd].p);
}
void access(int x){
for(int y = 0; x; y = x, x = x[nd].p){
splay(x);
x[nd].c[RIGHT] = y;
update(x);
}
}
void make_root(int x){
access(x); splay(x); reverse(x);
}
int find_root(int x){
access(x); splay(x); for(; x[nd].c[LEFT]; x = x[nd].c[LEFT]); return x;
}
void trylink(int x, int y){
make_root(x);
if(find_root(y) != x) x[nd].p = y;
}
void split(int x, int y){
make_root(x); access(y); splay(y);
}
void trycut(int x, int y){
split(x, y);
if(y[nd].c[LEFT] == x) x[nd].p = y[nd].c[LEFT] = 0; update(y);
}
int V, Q, i;
int ch, u, v;
int main(){
scanf("%d%d", &V, &Q);
for(i = 1; i <= V; i++)
scanf("%d", a + i);
for(; Q; Q--){
scanf("%d%d%d", &ch, &u, &v);
switch(ch){
case 0:
split(u, v);
printf("%d\n", nd[v].v);
break;
case 1:
trylink(u, v);
break;
case 2:
trycut(u, v);
break;
case 3:
access(u);
splay(u);
a[u] = v;
update(u);
break;
}
}
return 0;
}
毕竟是个 LCT 的裸题,抄着模板写起来还是比较顺畅的。抄好后为了保证熟练,以后定下目标,每天编程前码一遍 LCT。
(scx: 这看起来很累啊,你能做到吗?)
大概就是码两个月后,如果平均能在 5 min 中内码完且没有错误,那么就可以适当降低频率。
当时我做 lydsy 的题时,是照着板子抄的,那道洛谷题时,我试着自己码了一遍,7 分钟码完了,还过了样例,然后交上去总是 WA ...
(scx: 感觉你好几次都是过了样例提交就 WA ...)
我也觉得奇怪,然后开始调我手码的 LCT (你要知道 LCT 码萎了不是那么好调的),然后先照着 AC 的那次代码对了 4 遍,可能是我眼力比较差,没查出来,然后开始疯狂调试 ... 调了 2 个多小时,才发现是一个旋转写炸了 ...
当时把
nd[y[nd].c[!d] = x[nd].c[d]].p = y;写成了
nd[y[nd].c[!d] = x[nd].c[d]].p = x;
结果浪费了 2 个小时……(代码技术差要人命啊)