动态树之详解

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


splay操作和 splay树的道理类似。

可能有人会有疑问,为什么不一直单旋呢?就是一直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;
}

好吧,言尽于此。。。


发布了54 篇原创文章 · 获赞 3 · 访问量 7万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章