樹鏈剖分學習總結
(一)什麼是"樹剖"
對於解決一類樹上的問題,可以將樹上問題轉化為區間上問題求解。樹鏈剖分是指將
樹剖成鏈以解決一些樹上難以解決的問題。
那麼,問題來了,應該按照怎樣的規則將樹剖分?
(二)怎樣"剖分"
本文中的樹鏈剖分按照"重鏈"進行剖分。
首先要了解幾個概念:
對於一個節點,定義:
重兒子:該節點的所有子節點中,包含子節點節點數目最大的節點(一個節點最多一個)
輕兒子:該節點除重兒子以外的子節點
重邊:連線該節點與重兒子的邊
輕邊:連線該節點與輕兒子的邊
重鏈:由重邊相接組成的鏈
輕鏈:由輕邊相接組成的鏈
樹鏈剖分一種剖分方式是重鏈剖分,使重鏈上的節點在重新編號後成為連續的一段區間,以方便維護。
按照"重鏈"剖分其實是一種啟發式剖分。此外,還有按照"長鏈"剖分。
援引巨佬部落格中的內容:
樹鏈剖分目的:樹路徑資訊維護。
將一顆樹劃分成若干條鏈,用資料結構去維護每條鏈,複雜度\(\Theta(n\log n)\)
劃輕重鏈目的:為了讓鏈上節點的編號連續。
我們在用線段樹維護鏈的時候,如果節點編號不連續,那麼就無法用線段樹。
(三)程式設計實現剖分
第一步,遍歷整棵樹,將每個節點,每條邊的資訊處理出來。
需要處理的資訊有:
\(f[x]\):記錄節點\(x\)的父節點
\(siz[x]\):記錄以節點\(x\)根的子樹大小
\(son[x]\)
:記錄節點\(x\)的重兒子
\(d[x]\):記錄節點\(x\)的深度
\(top[x]\):記錄\(x\)所在的重鏈的頂端節點(即\(x\)所在重鏈上深度最淺的節點。如果\(x\)不在重鏈上,那麼\(top[x] = x\))
\(id[x] = y\):記錄節點\(x\)的新編號\(y\)
\(rak[y] = x\):記錄新編號\(y\)對應的節點\(x\)
對於前四個資訊,很容易維護,只需要從根節點遞迴遍歷整棵樹即可。程式碼如下:
void dfs1(int x, int fa, int deep){ f[x] = fa, d[x] = deep, siz[x] = 1; for(int i=head[x]; i; i=Next[i]){ int y = ver[i]; if(y == fa) continue; dfs1(y, x, deep+1); siz[x] += siz[y]; if(siz[y] > siz[son[x]] or son[x] == 0) son[x] = y; } return ; }
對於後面兩組資訊,我們需要解決的問題是,如何保證重鏈上的節點有連續的一段編號?
聯想到\(dfs\)序的性質,只要每次遞迴遍歷的時候優先遍歷重兒子,給其編號,就能保證重鏈上的節點有連續的一段編號了。
對於輕兒子,我們可以將其看作只有一個節點的重鏈。程式碼如下:
void dfs2(int x, int t){//t記錄x所在重鏈的頂端節點
top[x] = t, id[x] = ++cnt;
if(!son[x]) return ;
dfs2(son[x], t);
for(int i=head[x]; i; i=Next[i]){
int y = ver[i];
if(y == son[x] or y == f[x]) continue;
dfs2(y, y);
}
return ;
}
至此,整棵樹剖分已完畢。我們得到了一個序列,不難探究這個序列的性質。
由於我們是按照求\(dfs\)序的方法對節點編號,因此,這個序列具有\(dfs\)序的特點。
特點一:對於一棵子樹,它所有節點的編號是連續的。
同時,由於我們優先為重兒子編號,該序列還具有的特點是:
特點二:對於一條重鏈,它所有節點的編號是連續的。
(四)樹鏈剖分的運用
1.求樹上兩個節點的\(LCA\)(最近公共祖先)
眾所周知,\(LCA\)可以使用倍增求解。時間複雜是預處理\(\Theta(n\log n)\)加上每次詢問\(\Theta(\log n)\)。
如果使用樹鏈剖分求解\(LCA\),可以更高效。那麼,如何運用呢?
\(LCA\)的倍增方法是將兩個節點根據預處理出來的陣列不斷地向上跳。
樹鏈剖分也預處理了一個指向\(x\)祖先的陣列\(top[x]\),是否可以仿照\(LCA\)的模式,不斷向上跳呢?
答案顯然是可以的。
如果\(x\),\(y\)一直向上走,肯定會走到一個公共節點。為了避免\(x,y\)"擦肩而過",每次我們只讓其中一個向上走。
怎麼走呢?如果\(x,y\)不在一條重鏈上,那麼就讓深度較深的節點走到該條重鏈頂端的父節點,交錯著走,直到\(x,y\)在一條重鏈上。
如果\(x,y\)在一條重鏈上,那麼它們的\(LCA\)顯然是它們兩個中深度較淺的節點。
可以證明,剖分出來的路徑不超過\(\Theta(\log n)\)條,因此,樹鏈剖分求\(LCA\)的時間複雜度是:預處理\(\Theta(n)\)加上查詢\(\Theta(\log n)\)
2.維護樹上一條路徑上的點權或邊權
設這條路徑的兩個端點分別為\(x,y\)。由樹的性質可以知道,\(x,y\)之間只存在唯一的一條路徑。這條路徑是\(x\to LCA(x,y)\to y\)。
因此,我們只需要在求\(LCA(x,y)\)的同時,將經過的所有重鏈進行維護即可。
假設用線段樹進行區間維護,那麼時間複雜度為:
線段樹區間修改複雜度\(\Theta(\log n)\),樹剖求\(LCA(x,y)\)時間複雜度\(\Theta(\log n)\),總時間複雜度\(\Theta(\log^2n)\)
(五)幾道例題
1.洛谷P3384輕重鏈剖分
2.樹的統計
留坑待補...