動態樹問題是一種動態維護森林連通性,以及路徑上的信息的問題。目前我們可以利用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;
}
好吧,言盡於此。。。