1. 程式人生 > 實用技巧 >淺談樹鏈剖分

淺談樹鏈剖分

我們知道一顆樹的dfs序可以有多種,所以可以通過一些暗箱操作把dfs序給安排了,從而能夠更好的維護樹上的區間資訊

什麼是樹剖

樹剖的核心是恰當的把樹剖分成若干條鏈,鏈拼接起來就成了一段區間,然後利用資料結構來維護。

一些概念

重兒子:子樹節點最多的兒子

輕兒子:重兒子以外的兒子

重邊:父節點與重兒子所連的邊

輕邊:父節點與輕兒子所連的邊

重鏈:重邊組成的路徑

輕鏈:輕邊組成的路徑

重鏈頭:一條重鏈中深度最淺的點。特別地,與輕邊相連的葉子節點也算重鏈頭

圖中加粗邊為重邊,紅點為重鏈頭

樹鏈剖分流程

第一步,對這棵樹進行dfs,處理出每個節點的父節點、深度、重兒子

void dfs1(int u)
{
	size[u]=1; //size表示子樹大小,初始size為1 
	for(int x=Root[u];x!=0;x=Next[x])
		if(v[x]!=fa[u]){
			d[v[x]]=d[u]+1;fa[v[x]]=u; //處理深度和父親節點 
			dfs1(v[x]);size[u]+=size[v[x]]; //子節點size值處理後,更新父節點size 
			if(size[v[x]]>son[u])son[u]=v[x]; //取size最大的為重兒子 
		}
}

第二步,連線重鏈,安排dfs序

void dfs2(int u,int t)//u為當前節點,t為u所在重鏈的重鏈頭 
{
	id[u]=++tot,cur[tot]=a[u],top[u]=t; //處理dfs序和重鏈頭
	//id為時間戳,cur為新dfs序對應的值,top為重鏈頭 
	if(son[u])dfs2(son[u],t);//先處理重兒子
	//注意,重兒子和它的父節點都是同一個重鏈頭 
	for(int x=Root[u];x!=0;x=Next[x])
		if(v[x]!=fa[u]&&v[x]!=son[u])//如果是輕鏈底端,則自己做重鏈頭 
			dfs2(v[x],v[x]);
} 

至此,我們對樹的剖分就結束了

接下來我們用線段樹來維護剖分好的樹

以查詢樹上\(x\)\(y\)的和為例

剛才的dfs維護了top陣列

於是可以通過不斷的向上跳到重鏈頭,直到\(x\)\(y\)位於一條重鏈,再用線段樹查詢出跳過的區間值。這樣就可以\(O(nlogn)\)的計算出\(x\)\(y\)的和。

int calsum(int x,int y)
{
	ll res=0;
	while(top[x]!=top[y]){ //兩點不在一條重鏈 
		if(d[top[x]]<d[top[y]]) //設x為深度較深的點 
			swap(x,y);
		res+=query(id[top[x]],id[x],1); //對x所在重鏈求和 
		x=fa[top[x]]; //跳到重鏈頭的父親,也就是下一條重鏈上 
	}	
	if(d[x]>d[y])swap(x,y);  
	res+=query(id[x],id[y],1); //計算同一條重鏈上x到y的區間和 
	return res;
} 

一些例題

輕重鏈剖分
樹的統計

樹鏈剖分模板題

GSS7

樹剖+線段樹綜合應用

軟體包管理器

樹剖修改題