定義
動態樹問題 ,是一類要求維護一個有根樹森林,支援對樹的分割, 合併等操作的問題。
由 \(RobertE.Tarjan\) 為首的科學家們提出解決演算法 \(Link-Cut Trees\),簡稱 \(lct\)。
在處理樹上的很多詢問問題的時候,我們常常會用到輕重鏈剖分,但是它只能維護當樹的形態保持不變的時候的資訊。
那麼在樹形態發生變化的時候,輕重鏈剖分就失去了效果,這個時候我們就要用到 \(lct\)。
個人感覺這篇部落格講的不錯
前置知識:spaly
\(splay\) 是 \(lct\) 的輔助樹
如果不會 \(splay\) 可以去我的部落格學習一下
性質
\(1\)、和輕重鏈剖分相同的是,\(lct\) 也對要維護的樹進行了劃分,將鏈分為了實鏈和虛鏈。
每一個 \(Splay\) 維護的是一條從上到下按在原樹中深度嚴格遞增的路徑,且中序遍歷 \(Splay\) 得到的每個點的深度序列嚴格遞增。
\(2\)、每個節點包含且僅包含於一個 \(Splay\) 中
\(3\)、邊分為實邊和虛邊,實邊包含在 \(Splay\) 中,而虛邊總是由一棵 \(Splay\) 指向另一個節點(指向該 \(Splay\) 中中序遍歷最靠前的點在原樹中的父親)。
如果一個節點有多個兒子,那麼該節點只會認用實邊相連的那個兒子,對於虛邊相連的兒子,則會認父不認子。
操作
1、結構體定義
個人的寫法是把 \(lct\) 封裝到了結構體裡
int ch[maxn][2],fa[maxn],val[maxn],sum[maxn],top,sta[maxn],rev[maxn];
//ch[0/1]:左/右兒子 fa:父親節點 val:該節點的權值
//sum:該節點及其子樹的權值之和 sta&top:push_down時用 rev:翻轉標記
2、標記上傳和下放
類似於線段樹
要注意的是,線段樹的父親節點並不是一個真實的節點,而 \(spaly\) 的父親節點代表了原樹中真實存在的一個節點
void push_up(rg int da){
sum[da]=sum[ch[da][0]]^sum[ch[da][1]]^val[da];
}
void push_down(rg int da){
rg int lc=ch[da][0],rc=ch[da][1];
if(rev[da]){
rev[lc]^=1,rev[rc]^=1,rev[da]^=1;
std::swap(ch[da][0],ch[da][1]);
}
}
3、isroot操作
判斷某個節點是否是該節點所在的 \(splay\) 的根節點
利用了性質 \(3\)
bool isroot(rg int da){
return (ch[fa[da]][0]!=da)&&(ch[fa[da]][1]!=da);
}
4、splay操作
和 \(splay\) 板子的區別就是多了 \(push\_down\) 操作
void xuanzh(rg int x){
rg int y=fa[x];
rg int z=fa[y];
rg int k=(ch[y][1]==x);
if(!isroot(y)){
ch[z][ch[z][1]==y]=x;
}
fa[x]=z;
ch[y][k]=ch[x][k^1];
fa[ch[x][k^1]]=y;
ch[x][k^1]=y;
fa[y]=x;
push_up(y);
push_up(x);
}
void splay(rg int x){
sta[top=1]=x;
for(rg int i=x;!isroot(i);i=fa[i]) sta[++top]=fa[i];
for(rg int i=top;i>=1;i--) push_down(sta[i]);
//把需要下傳標記的提前存起來一起修改
while(!isroot(x)){
rg int y=fa[x];
rg int z=fa[y];
if(!isroot(y)){
(ch[z][1]==y)^(ch[y][1]==x)?xuanzh(x):xuanzh(y);
}
xuanzh(x);
}
}
5、access操作
比較重要的一個操作
目的是打通根節點到指定節點的實鏈,使得一條中序遍歷以根開始、以指定點結束的 \(Splay\) 出現
void access(rg int x){
for(rg int y=0;x;y=x,x=fa[x]){
splay(x);
ch[x][1]=y;
push_up(x);
}
}
6、makeroot操作
使某一個節點成為整個聯通塊的根
\(access\) 之後已經打通了一條當前點到根節點的路徑
此時 \(x\) 在 \(Splay\) 中一定是深度最大的點
把 \(x\ splay\) 一下,\(x\) 將沒有右子樹(性質\(1\))
於是翻轉整個 \(Splay\),使得所有點的深度都倒過來了
\(x\) 沒了左子樹,成了深度最小的點(根節點),達到了我們的目的
要注意的是,換根操作之後,原樹的形態就改變了
void makeroot(rg int x){
access(x);
splay(x);
rev[x]^=1;
push_down(x);
}
7、findroot操作
找出指定點所在的聯通塊的根節點
和上一個操作一樣,只不過這一次我們不再進行翻轉操作,而是一直跳左子樹
最後跳到的節點就是根
int findroot(rg int x){
access(x);
splay(x);
while(ch[x][0]){
push_down(x);
x=ch[x][0];
}
splay(x);
return x;
}
8、split操作
提取 \(x\) 到 \(y\) 的路徑
先讓 \(x\) 成為根節點,再打通 \(y\) 到根節點的路徑
void split(rg int x,rg int y){
makeroot(x);
access(y);
splay(y);
}
9、刪邊、連邊操作
先讓 \(x\) 成為根節點,再判斷聯通性
void link(rg int x,rg int y){
makeroot(x);
if(findroot(y)!=x) fa[x]=y;
}
void cut(rg int x,rg int y){
makeroot(x);
if(findroot(y)==x && fa[y]==x && ch[y][0]==0){
fa[y]=ch[x][1]=0;
push_up(x);
}
}
10、求LCA操作
兩遍 \(access\)
int access(rg int x){
for(rg int y=0;x;y=x,x=fa[x]){
splay(x);
ch[x][1]=y;
push_up(x);
}
return y;// 多了一個返回值
}
然後求 \(LCA\) 的過程就出來了。
int LCA(int x,int y){
if(findroot(x)!=findroot(y))// 必須的特判。
return -1;
access(x);
return access(y);
}
完整模板
#include<cstdio>
#include<iostream>
#define rg register
inline int read(){
rg int x=0,fh=1;
rg char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') fh=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*fh;
}
const int maxn=1e6+5;
int n,m;
struct LCT{
int ch[maxn][2],fa[maxn],val[maxn],sum[maxn],top,sta[maxn],rev[maxn];
void push_up(rg int da){
sum[da]=sum[ch[da][0]]^sum[ch[da][1]]^val[da];
}
void push_down(rg int da){
rg int lc=ch[da][0],rc=ch[da][1];
if(rev[da]){
rev[lc]^=1,rev[rc]^=1,rev[da]^=1;
std::swap(ch[da][0],ch[da][1]);
}
}
bool isroot(rg int da){
return (ch[fa[da]][0]!=da)&&(ch[fa[da]][1]!=da);
}
void xuanzh(rg int x){
rg int y=fa[x];
rg int z=fa[y];
rg int k=(ch[y][1]==x);
if(!isroot(y)){
ch[z][ch[z][1]==y]=x;
}
fa[x]=z;
ch[y][k]=ch[x][k^1];
fa[ch[x][k^1]]=y;
ch[x][k^1]=y;
fa[y]=x;
push_up(y);
push_up(x);
}
void splay(rg int x){
sta[top=1]=x;
for(rg int i=x;!isroot(i);i=fa[i]) sta[++top]=fa[i];
for(rg int i=top;i>=1;i--) push_down(sta[i]);
while(!isroot(x)){
rg int y=fa[x];
rg int z=fa[y];
if(!isroot(y)){
(ch[z][1]==y)^(ch[y][1]==x)?xuanzh(x):xuanzh(y);
}
xuanzh(x);
}
}
void access(rg int x){
for(rg int y=0;x;y=x,x=fa[x]){
splay(x);
ch[x][1]=y;
push_up(x);
}
}
void makeroot(rg int x){
access(x);
splay(x);
rev[x]^=1;
push_down(x);
}
int findroot(rg int x){
access(x);
splay(x);
while(ch[x][0]){
push_down(x);
x=ch[x][0];
}
splay(x);
return x;
}
void split(rg int x,rg int y){
makeroot(x);
access(y);
splay(y);
}
void link(rg int x,rg int y){
makeroot(x);
if(findroot(y)!=x) fa[x]=y;
}
void cut(rg int x,rg int y){
makeroot(x);
if(findroot(y)==x && fa[y]==x && ch[y][0]==0){
fa[y]=ch[x][1]=0;
push_up(x);
}
}
}lct;
int main(){
n=read(),m=read();
for(rg int i=1;i<=n;i++) lct.sum[i]=lct.val[i]=read();
rg int aa,bb,cc;
for(rg int i=1;i<=m;i++){
aa=read(),bb=read(),cc=read();
if(aa==0){
lct.split(bb,cc);
printf("%d\n",lct.sum[cc]);
} else if(aa==1){
lct.link(bb,cc);
} else if(aa==2){
lct.cut(bb,cc);
} else {
lct.splay(bb);
lct.val[bb]=cc;
}
}
return 0;
}
複雜度證明
一些題目
型別一、維護鏈資訊
注意區間標記先乘後除
然後就和普通的線段樹一樣標記下放即可
如果沒有刪邊和加邊操作,也可以用樹鏈剖分實現,但是複雜度要多一個 \(log\)
有的時候要維護的資訊不是在點上而是在邊上
此時我們就需要把邊看成點
例如有一條邊 \((u,v)\),編號為 \(id\)
那麼我們需要在 \(lct\) 中連兩條邊 \((u,id+n)(v,id+n)\)
邊的資訊儲存在 \(id+n\) 這個點上
型別二、動態維護聯通性
用可撤銷並查集按秩合併也可以做
換成 \(lct\) 也是一個板子
只要判斷兩個點 \(findroot\) 得到的值是否一樣即可
由於圖中會出現邊雙,不能重複計算,所以需要將邊雙縮點
用並查集維護當前的點屬於哪一個雙聯通分量
考慮合併 \(x\) 到 \(y\) ,如果 \(x\) 和 \(y\) 在 \(lct\) 上不是聯通的,那麼直接連邊
否則我們先取出 \(x\) 到 \(y\) 的路徑,將路徑上所有權值都加到 \(y\) 上,同時把路徑上所有點的父親設為 \(y\)
要注意的是每次操作之前都要在並查集裡找一下祖先
型別三、最小生成樹一類的問題
這種題大部分都需要在邊權上維護一個最大/最小值,然後按照邊權從大到小/從小到大排序
遍歷到一條邊時,如果這條邊所連的兩個點已經在同一個同一個聯通塊中,根據需要刪邊/加邊
型別四、維護子樹資訊
因為 \(LCT\) 中的兒子有實兒子和虛兒子之分
\(splay\) 中儲存的只是實兒子的資訊
所以我們要新開一個變數 \(siz2\) 記錄虛子樹的大小
相應地一些函式也要被修改
struct LCT{
int ch[maxn][2],siz2[maxn],siz[maxn],top,sta[maxn],rev[maxn],wz[maxn],fa[maxn];
void push_up(rg int da){
siz[da]=siz[ch[da][1]]+siz[ch[da][0]]+1+siz2[da];//1
}
void push_down(rg int da){
rg int lc=ch[da][0],rc=ch[da][1];
if(rev[da]){
rev[lc]^=1,rev[rc]^=1,rev[da]^=1;
std::swap(ch[da][0],ch[da][1]);
}
}
bool isroot(rg int da){
return ch[fa[da]][0]!=da&&ch[fa[da]][1]!=da;
}
void xuanzh(rg int x){
rg int y=fa[x];
rg int z=fa[y];
rg int k=(ch[y][1]==x);
if(!isroot(y)){
ch[z][ch[z][1]==y]=x;
}
fa[x]=z;
ch[y][k]=ch[x][k^1];
fa[ch[x][k^1]]=y;
ch[x][k^1]=y;
fa[y]=x;
push_up(y);
push_up(x);
}
void splay(rg int x){
top=1;
sta[top]=x;
for(rg int i=x;!isroot(i);i=fa[i]) sta[++top]=fa[i];
for(rg int i=top;i>=1;i--) push_down(sta[i]);
while(!isroot(x)){
rg int y=fa[x];
rg int z=fa[y];
if(!isroot(y)){
(ch[z][1]==y)^(ch[y][1]==x)?xuanzh(x):xuanzh(y);
}
xuanzh(x);
}
}
void access(rg int x){
for(rg int y=0;x;y=x,x=fa[x]){
splay(x);
siz2[x]+=siz[ch[x][1]]-siz[y];//2
ch[x][1]=y;
push_up(x);
}
}
void makeroot(rg int x){
access(x);
splay(x);
rev[x]^=1;
push_down(x);
}
void split(rg int x,rg int y){
makeroot(x);
access(y);
splay(y);
}
void link(rg int x,rg int y){
makeroot(x);
makeroot(y);
fa[x]=y;
siz2[y]+=siz[x];//3
}
void cut(rg int x,rg int y){
split(x,y);
fa[x]=ch[y][0]=0;
push_up(y);
makeroot(x);
makeroot(y);
}
}lct;
型別五、查詢k級祖先
若設每個點的點權均為 \(1\),刪除時把點權置為 \(0\)
則這樣得到的 \(k\) 級祖先就是到這個點的路徑上權值和為 \(k\) 的祖先
所以在 \(lct\) 上二分查詢即可
程式碼
int find(rg int now,rg int ke){
access(now);
splay(now);
if(!now || sum[ch[now][0]]<ke) return 0;
if(ke==0) return now;
now=ch[now][0];
while(now){
if(sum[ch[now][1]]>=ke) now=ch[now][1];
else if(sum[ch[now][1]]+val[now]==ke) return splay(now),now;
else {
ke-=(sum[ch[now][1]]+val[now]);
now=ch[now][0];
}
}
return 233;
}