动态树问题是一种动态维护森林连通性,以及路径上的信息的问题。目前我们可以利用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; }