樹鏈剖分~~不詳細~~講解
前置技能:線段樹、DFS
當我第一次聽到“樹鏈剖分”這個算法的時候,感覺它一定很高大上。現在看來,它確實很高大上,不過也十分的暴力(個人認為,不喜勿噴)
基本概念:
樹鏈剖分,計算機術語,指一種對樹進行劃分的算法,它先通過輕重邊剖分將樹分為多條鏈,保證每個點屬於且只屬於一條鏈,然後再通過數據結構(樹狀數組、SBT、SPLAY、線段樹等)來維護每一條鏈。
————某度百科
百度百科對什麽是樹剖已經說的很明白了,接下來我們再了解一下其他的概念。
- 重兒子:對於每一個非葉子節點,它的兒子中子樹節點最多的兒子
- 輕兒子:對於每一個非葉子節點,它的除重兒子以外的兒子
- 重邊:父親節點連向重兒子的邊
- 輕邊:父親節點連向輕兒子的邊
- 重鏈:由多條重邊連成的一條樹鏈
- 輕鏈:由多條輕邊連成的一條樹鏈
在這張圖片中,帶紅點的就是輕兒子,其余為重兒子;加粗的邊為重邊,其余的為輕邊;$1->14/;2->11/;3->7 $的路徑為重鏈,其余的為輕鏈。
前面某度已經說了,樹鏈剖分要通過輕重邊剖分將樹分為多條鏈,那麽它是怎麽找出輕重邊,又是怎麽剖分的的呢?不要著急,我們接著講
實現方法
先來說一說我們需要求哪些東西
變量 | 含義 |
---|---|
\(f[i]\) | 結點\(i\)的父親 |
\(son[i]\) | 結點\(i\)的重兒子(如果有\(i\)有兩個及以上的重兒子,則隨便指定一個) |
\(size[i]\) | 結點\(i\)的子樹大小 |
\(deth[i]\) | 結點\(i\)的深度 |
\(top[i]\) | 結點\(i\)所在的重鏈的頂端(若\(i\)為輕兒子,則\(top[i]\)等於它本身) |
\(pos[i]\) | 結點\(i\)的新編號(可以理解為點\(i\)對應的\(rank\)數組的下標) |
\(rank[i]\) | 編號\(i\)對應的樹上的結點的點權 |
其中,前四個變量可以通過一次\(DFS\)求出,其余三個可以在第一次\(DFS\)的基礎上再通過一次\(DFS\)求出
代碼是這樣滴:
void dfs1(int now,int fa){ f[now]=fa, deth[now]=deth[fa]+1, size[now]=1; for(int i=head[now];i;i=e[i].nex){ int to=e[i].t; if(to==fa) continue; dfs1(to,now); size[now]+=size[to]; if(size[to]>size[son[now]]) son[now]=to; } } void dfs2(int now,int topp){ top[now]=topp, pos[now]=++dfn, rank[dfn]=a[now]; //a[i]表示結點i的點權 if(!son[now]) return ; dfs2(son[now],topp); for(int i=head[now];i;i=e[i].nex){ int to=e[i].t; if(to!=son[now]&&to!=f[now]) dfs2(to,to); } }
在我們進行第二次\(DFS\)的時候,我們是優先搜索重兒子,這是為了保證重鏈在\(rank\)數組裏的連續性,除了重鏈,一顆子樹的編號在\(rank\)數組裏也是連續的。
為什麽要這麽做?接下來你就知道了。
到此,我們的樹鏈剖分就講完了。可是,現在我們求出了這麽多東西,它們能幹什麽呢?
還記得一開始某度百科上說過可以“通過數據結構(樹狀數組、SBT、SPLAY、線段樹等)來維護每一條鏈”嗎?沒錯,在求出了這麽多東西後,我們就可以用我們所熟悉的數據結構來瞎搞這顆樹了(大霧
為了方便理解+應用廣泛,我們以線段樹為例來講一下樹鏈的維護(其實是因為博主太蒟,只會線段樹)
假設題目讓我們進行以下操作:
- 將樹從x到y結點最短路徑上所有節點的值都加上z
- 求樹從x到y結點最短路徑上所有節點的值之和
- 將以x為根節點的子樹內所有節點值都加上z
- 求以x為根節點的子樹內所有節點值之和
上文我們說過:重鏈在\(rank\)數組裏是連續的,一顆子樹在\(rank\)數組裏也是連續的。所以我們可以用線段樹通過多次區間修改和多次區間查詢來搞定這四個操作。
首先是線段樹:
其實線段樹的一切都沒什麽變化,該怎麽打還是怎麽打,只不過要維護的數組變成我們剖出來的\(rank\)數組
代碼如下:
void build(int l,int r,int p){ //建樹
if(l==r){
tree[p]=rank[l]; return ; //要註意這裏的數組是rank
}
build(l,mid,ls); build(mid+1,r,rs);
tree[p]=tree[ls]+tree[rs];
}
void down(int l,int r,int p){ //下傳懶標記(我太蒟了,不會標記永久化)
tag[ls]+=tag[p]; tag[rs]+=tag[p];
tree[ls]+=(mid-l+1)*tag[p];
tree[rs]+=(r-mid)*tag[p];
tag[p]=0;
}
void update(int l,int r,int p,int nl,int nr,ll k){ //區間修改
if(nl<=l&&nr>=r){
tag[p]+=k; tree[p]+=(r-l+1)*k;
return ;
}
down(l,r,p);
if(nl<=mid) update(l,mid,ls,nl,nr,k);
if(nr>mid) update(mid+1,r,rs,nl,nr,k);
tree[p]=tree[ls]+tree[rs];
}
ll query(int l,int r,int p,int nl,int nr){ //區間查詢
ll ans=0;
if(nl<=l&&nr>=r) return tree[p];
down(l,r,p);
if(nl<=mid) ans+=query(l,mid,ls,nl,nr);
if(nr>mid) ans+=query(mid+1,r,rs,nl,nr);
return ans;
}
那這棵線段樹該怎麽用呢?
如果點\(x\)和\(y\)不在一條重鏈上,就讓它們一直跳,直到跳到一條重鏈上。為了防止越跳越遠,我們讓深度更深的先跳到另一條鏈上。在跳的時候,因為重鏈在數組中是連續的,我們就可以用線段樹進行區間更改/查詢來處理這一部分,通過多次區間操作,就能夠實現這操作1、2。
void upd(int x,int y,ll k){ //將樹從x到y結點最短路徑上所有節點的值都加上z
while(top[x]!=top[y]){ //如果不在一條重鏈上
if(deth[top[x]]<deth[top[y]]) swap(x,y);
update(1,n,1,pos[top[x]],pos[x],k);
x=f[top[x]]; //讓更深的跳上來,跳到另一條鏈上,順便加上區間修改
}
//如果在一條鏈上
if(deth[x]>deth[y]) swap(x,y);
update(1,n,1,pos[x],pos[y],k); //則處理一下兩節點之間的區間
}
ll sum(int x,int y){ //查詢操作和修改是一樣的……
ll ans=0;
while(top[x]!=top[y]){
if(deth[top[x]]<deth[top[y]]) swap(x,y);
ans+=query(1,n,1,pos[top[x]],pos[x]);
x=f[top[x]];
}
if(deth[x]>deth[y]) swap(x,y);
ans+=query(1,n,1,pos[x],pos[y]);
return ans;
}
對於操作3、4,則更為簡單。因為子樹在數組中是連續的,我們又知道每棵子樹的大小,所以直接一波線段樹就可以了
update(1,n,1,pos[x],pos[x]+size[x]-1,y); //將以x為根節點的子樹內所有節點值都加上z
query(1,n,1,pos[x],pos[x]+size[x]-1); //求以x為根節點的子樹內所有節點值之和
到此,樹剖就真的講完了,不知眾看官看懂了多少……
推薦題目
- Luogu【模板】樹鏈剖分 (也就是我們講的例題)
- HAOI2015 樹上操作
- NOI2015 軟件包管理器
以上這些都是一些裸題。樹剖本身不難理解,但因為代碼較長,比較容易寫錯……又全是遞歸,不好調試……所以要多練……
參考博客
樹鏈剖分原理和實現 —— \(banananana\)
樹鏈剖分詳解—— \(ChinHhh\)
樹鏈剖分詳解—— \(communist\)
樹鏈剖分~~不詳細~~講解