1. 程式人生 > 實用技巧 >樹鏈剖分學習總結

樹鏈剖分學習總結

(一)什麼是"樹剖"

對於解決一類樹上的問題,可以將樹上問題轉化為區間上問題求解。樹鏈剖分是指將
樹剖成鏈以解決一些樹上難以解決的問題。

那麼,問題來了,應該按照怎樣的規則將樹剖分?

(二)怎樣"剖分"

本文中的樹鏈剖分按照"重鏈"進行剖分。

首先要了解幾個概念:

對於一個節點,定義:

重兒子:該節點的所有子節點中,包含子節點節點數目最大的節點(一個節點最多一個)

輕兒子:該節點除重兒子以外的子節點

重邊:連線該節點與重兒子的邊

輕邊:連線該節點與輕兒子的邊

重鏈:由重邊相接組成的鏈

輕鏈:由輕邊相接組成的鏈

樹鏈剖分一種剖分方式是重鏈剖分,使重鏈上的節點在重新編號後成為連續的一段區間,以方便維護。

按照"重鏈"剖分其實是一種啟發式剖分。此外,還有按照"長鏈"剖分。

援引巨佬部落格中的內容:

樹鏈剖分目的:樹路徑資訊維護。

將一顆樹劃分成若干條鏈,用資料結構去維護每條鏈,複雜度\(\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.樹的統計

留坑待補...