1. 程式人生 > 其它 >樹鏈剖分の學習筆記

樹鏈剖分の學習筆記

樹鏈剖分是一種對樹進行劃分的演算法,將樹劃分為若干個鏈,以便維護樹上路徑的資訊。 也就是說,我們將樹劃分為若干個線性結構,使用一些資料結構來維護它,如:樹狀陣列,線段樹,splay等。

一.樹鏈剖分的概念

樹鏈剖分是一種對樹進行劃分的演算法,將樹劃分為若干個鏈,以便維護樹上路徑的資訊。

也就是說,我們將樹劃分為若干個線性結構,使用一些資料結構來維護它,如:樹狀陣列,線段樹,splay等。


二.樹鏈剖分的作用

前面我們提到,我們將樹劃分為多個線性結構,用資料結構來維護它,所以我們可以很快捷地對於樹上的某些資訊進行修改或者查詢等操作,例如:修改某條樹上路徑的所有點權;查詢某條樹上路徑點權的極值、和、等。


三.重鏈剖分

我們先要了解一些很重要的定義。

重子節點:其子節點中子樹大小最大的節點,若有多個,任取其一。若沒有子節點,那麼久沒有重子節點。

輕子節點:其子節點中除重子節點以外的所有子節點。

重邊:從當前節點到重子節點的邊。

輕邊:從當前節點到輕子節點的邊。

重鏈:若干條相連線的重邊

這裡給出一張摘自oi-wiki的圖片供讀者理解。


四.應用

我們先給出一些變數名的定義:

top[i]//表示節點i所在的重鏈的頂部節點。
dfn[i]//表示節點i的dfs序,也是節點i線上段樹中的序號。
dep[i]//表示節點i的深度。
fa[i]//表示節點i的父親。
siz[i]//表示節點i的子樹的節點個數。
son[i]//表示節點i的重子節點。
sgt[i]//表示dfs序所對應的節點編號。

求兩個節點 \(x\)\(y\) 的路徑上的點權之和:

int query_tree (int x, int y) {
	int ans = 0;
	
	while (top[x] != top[y]) {
   	//x和y不在同一條重鏈上。
		if (dep[top[x]] < dep[top[y]]) {
			swap (x, y);
		} 
		
		ans += query (1, dfn[top[x]], dfn[x]);
        //用線段樹維護鏈上資訊。
		
		x = fa[top[x]];
        //將x設為原重鏈鏈頭的父節點,繼續從輕邊迴圈。
	}
	
	if (dep[x] > dep[y]) {
		swap (x, y);
	}
    //x和y已經在同一條重鏈上了。
	
	ans += query (1, dfn[x], dfn[y]);
    //用線段樹維護鏈上資訊。
	
	return ans;
}

這個操作就很像 \(LCA\) ,我們使用了 \(top\) 進行加速。

注意,我們每次迴圈的時候只能讓同一個節點向上跳,否則會出現少計算答案的情況。

同理,修改樹上某條路徑的點權也是一樣的。


求以節點 \(x\) 為根節點的子樹內所有點權和:

我們知道,我們將樹劃分為了多個線性結構,也就是鏈。

於是,我們就可以利用一些資料結構進行維護。

所以,我們在查詢子樹和的時候,只需要查詢一段區間的和。

這裡我用線段樹進行查詢,程式碼如下:

query (1, dfn[x], dfn[x] + siz[x] - 1);

五.例題

P3384 【模板】輕重鏈剖分/樹鏈剖分

這道題就是樹鏈剖分的模板題。

對於 \(3\)\(4\) 操作,我們可以將其化為線性結構進行維護。

\(1\)\(2\) 操作,我們只需要進行樹上操作即可。

操作 \(1\) :

void addtree (int x, int y, int k) {
	while (top[x] != top[y]) {
	//不在同一條重鏈上。
		if (dep[top[x]] < dep[top[y]]) {
			swap (x, y);
		}
		
		addS (1, dfn[top[x]], dfn[x], k);
        //線段樹區間修改。
		
		x = fa[top[x]];
        //向上跳轉。
	}
	
	if (dep[x] > dep[y]) {
		swap (x, y);
	}
    //現在節點x和y在同一條重鏈上。
	
	addS (1, dfn[x], dfn[y], k);
    //線段樹區間修改。
}

操作 \(2\)

int query_tree (int x, int y) {
	int ans = 0;
	
	while (top[x] != top[y]) {
	//不在同一條重鏈上。
		if (dep[top[x]] < dep[top[y]]) {
			swap (x, y);
		} 
		
		ans += query (1, dfn[top[x]], dfn[x]);
        //線段樹區間查詢。            
		ans %= p;
		
		x = fa[top[x]];
        //向上跳轉。
	}
	
	if (dep[x] > dep[y]) {
		swap (x, y);
	}
    
    //節點x和y在同一條重鏈上的時候。
	
	ans += query (1, dfn[x], dfn[y]);
	//線段樹區間查詢。
    
	return ans %p;
}

操作 \(3\)

addS (1, dfn[x], dfn[x] + siz[x] - 1, y % p);
//運用線段樹的區間修改。

操作 \(4\)

printf ("%d\n", query (1, dfn[x], dfn[x] + siz[x] - 1) %p);
//運用線段樹的區間查詢。

完整程式碼。


P3178 [HAOI2015]樹上操作