動態樹之詳解

動態樹問題是一種動態維護森林連通性,以及路徑上的信息的問題。目前我們可以利用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萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章