动态树问题是一种动态维护森林连通性,以及路径上的信息的问题。目前我们可以利用link-cut-tree进行求解。
最近发现自己以前写的那个版本实在是太渣了,于是膜拜神犇代码写了新的版本。
我们首先简单解释一下Link-Cut-Tree的原理。
将树划分为若干重链,重链之间用轻链相连接。
每一条重链用一颗Splay树维护。Splay的序关系由点的深度确定。
划分的依据?关键在于Access操作。
这个操作后,当前节点到根的这一条路径将成为一条重链。而且重链上只存在这条路径上的点!
具体实现见后文所述。
依旧使用指针结构,不同的是每个节点只记录自己的父亲pa。当他是父亲的左右儿子时,他与父亲都在重链上,否则他是重链的根。
这样做的常系数很小,至少比起之前是强多了。代码貌似变得更加好写?
我以一道例题BZOJ3282 Tree举例。
这道题要求支持修改点权,求路径的权值的异或和,并支持Link,Cut操作。值得注意的是Link,Cut操作不一定合法。
点的定义:
#define N 300010
#define l ch[0]
#define r ch[1]
struct Node {
Node *ch[2], *pa;
int val, sum;
bool rev;
Node():val(0),sum(0),rev(0){}
inline bool d() {
return this == pa->r;
}
inline void sc(Node *p, bool d) {
ch[d] = p;
p->pa = this;
}
inline bool isroot();
inline void up();
inline void down();
inline void revIt();
}Tnull, *null = &Tnull, mem[N], *C = mem;
Node* Newnode(int x) {
C->l = C->r = C->pa = null;
C->val = C->sum = x;
C->rev = 0;
return C++;
}
inline bool Node::isroot() {
return this != pa->l && this != pa->r;
}
inline void Node::up() {
sum = l->sum ^ r->sum ^ val;
}
inline void Node::revIt() {
rev ^= 1;
swap(l, r);
}
inline void Node::down() {
if (rev) {
l->revIt();
r->revIt();
rev = 0;
}
}
我自认为这份代码看着还是很舒心的~~~
利用了静态数组优化,看Newnode就知道了。
isroot函数也不难理解。
d函数是表示若该点在splay中是右儿子,返回1,否则返回0.
我还是觉得l,r的宏定义亮爆了。
另外注意null的初值,也就是Node构造函数中的初值。
下面我们看比较关键的Splay操作啦~~~
(1)旋转
void rot(Node *q) {
Node *fa = q->pa;
bool d = q->d();
fa->sc(q->ch[!d], d);
fa->up();
q->pa = fa->pa;
if (!fa->isroot())
fa->pa->ch[fa->d()] = q;
q->sc(fa, !d);
}
速来点赞!
首先联想一下Splay的旋转图解,事情就会变得很轻松。
首先当前我们要旋转的是q,他的父亲是fa(此时这两个点一定在一条重链上)
以q是fa的左儿子举例(time for imagining..)
q的右儿子代替fa的左儿子(本来是q),fa的子树发生变化,因此更新一下fa.
然后本来是q代替fa的位置,不过考虑到fa有可能是这个重链的根,如果是这样的话,fa->pa的儿子不应该改变。(否则就会变成一条重链上了)
然后fa代替q右儿子的位置。
旋转的过程很轻松吧?
这里q的子树情况也发生了变化,为什么不更新q呢?
没有必要,后文会进行解释。
(2)标记下传以及splay操作
暂且不论其他的标记,有一个标记是Link-Cut-Tree中几乎必不可少的标记,即-区间反转标记。下文再解释。用rev表示。
Splay能干些什么?就是令某个节点旋转到其所在的Splay树的根。
我们发现影响的就是当前节点到当前splay根节点上的全部节点,我们用栈按照深度从小到大依次将标记下传即可。
随后我们看Splay操作,太简洁了!而且很容易理解。
void pushpath(Node *q) {
static Node* stack[N];
int top = 0;
for(; !q->isroot(); q = q->pa)
stack[++top] = q;
stack[++top] = q;
for(int i = top; i >= 1; --i)
stack[i]->down();
}
void splay(Node *q) {
pushpath(q);
while(!q->isroot()) {
if (q->pa->isroot())
rot(q);
else
rot(q->d() == q->pa->d() ? q->pa : q), rot(q);
}
q->up();
}
可能有人会有疑问,为什么不一直单旋呢?就是一直rot(q)直到q->isroot()=1为止?
这样虽然也是对的,然而复杂度就不能保证了。容易被奇葩数据卡掉。
上述的双旋操作可以保证均摊O(logn),就不证明了。
现在解释为什么在旋转之后不更新q的原因:事实上我们只在splay时才用到旋转,那么直到q到了最终的位置再更新就好了,每旋转一次就更新也没有意义。
下面是Link-Cut-Tree的核心操作-Access!忘了他是干什么的往上翻。
我们选择一个当前的点,将其旋到其所在Splay树的根,并将其右子树切掉,并换成上一条找到的重链的根。事实上在当前写法下只需更换儿子就行了。
void Access(Node *q) {
Node *p = null;
while(q != null) {
splay(q);
q->r = p;
q->up();
p = q;
q = q->pa;
}
}
真心简洁。别忘了更新。
有了Access和Splay,我们能实现很多简单地操作。
这样就有了一下这些代码,相信很容易理解。
void Makeroot(Node *q) {//使一个点成为所在的一棵树的根(这棵树不是Splay树)
Access(q); //从q到根的路径成为一颗Splay
splay(q); //让q旋到根,此时q由于深度最大,因此q只有左子树
q->revIt();//让q机智的区间反转一发,这样q就只有右子树,那么q深度最小,就是根了,这里只打一个标记,交换一下就行
}
Node* Findroot(Node *q) {//寻找其所在树的根
while(q->pa != null) //就是无脑往上找,但这样做并不慢,反而比一些看起来高级的方法快一些
q = q->pa;
return q;
}
void Link(Node *p, Node *q) {
if (Findroot(p) == Findroot(q)) //若已经在一棵树上,退出
return;
Makeroot(p); //不解释
p->pa = q;
}
void Cut(Node *p, Node *q) {
if (p == q || Findroot(p) != Findroot(q))
return;
Makeroot(p);
Access(q);
splay(q); //不解释
if (q->l == p) {
q->l = p->pa = null;
q->up();
}
}
void Change(Node *q, int c) {
splay(q);
q->val = c;
q->up();
}
int getpath(Node *p, Node *q) {//很显然
Makeroot(p);
Access(q);
splay(q);
return q->sum;
}
好吧,言尽于此。。。