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

【樹鏈剖分】【學習筆記】

【樹鏈剖分】【學習筆記】

功能:它可以將一棵樹劃分成若干條鏈,從而將對樹的各種操作轉化為序列上操作,而序列上的操作我們就可以用一些高階資料結構如線段樹,平衡樹等來維護……

先看道例題吧

如題,已知一棵包含 N 個結點的樹(連通且無環),每個節點上包含一個數值,需要支援以下操作:
1 x y z,表示將樹從 x 到 y 結點最短路徑上所有節點的值都加上 z。
2 x y,表示求樹從 x 到 y 結點最短路徑上所有節點的值之和。
3 x z,表示將以 x 為根節點的子樹內所有節點值都加上 z。
4 x 表示求以 x 為根節點的子樹內所有節點值之和

分析

一共4種操作,按照操作物件可以分為兩類,一類是“兩點間的路徑”,另一類是對“一顆子樹”

操作型別也有兩種,修改,求和

我們先來考慮“子樹”的情況:

這裡就要先引入dfs序(dfn序)了,

dfs序:從根節點開始進行dfs對整棵樹進行遍歷,同時每訪問到一個節點,就將其加入到dfs序的末尾,最後形成的序列

性質:對於原樹的每顆子樹,其節點在dfs序上必是連續的一段

證明?:自己想想就知道了,因為在dfs的過程中,如果有一顆指標時刻指向當前節點,那麼當這個指標進入一顆子樹,只有在訪問完它的所有節點後才會退出,因此有上述結論

於是我們就把樹變成了序列,對每個節點[x],記以它為根節點的子樹的大小為size[x],它在dfs中的位置為pos[i],那它在dfs序上對應的位置就是[pos[i],pos[i]+size[x]-1],就可以直接用線段樹來維護了,單次操作logN

再來看第二種,對於“樹上的一條路徑”,我們如何操作呢?

這才進入了正題嘛,————重鏈剖分

我們依然想辦法將路徑上的操作轉為對一個序列的操作,但是問題是對於樹上任意一條路徑,其路徑上的的點對應的dfs序編號不再是連續的了

如下圖,對於2號和9號節點,其路徑在dfs序上就是完全不連續的,我們若是暴力對每個點操作,單次複雜度是O(N)的

那怎麼辦呢?

這就顯出樹剖的厲害了,我們依然採用dfs序,但是可以通過一種“特殊的”劃分方式,使得樹上任意一條路徑在dfs序中都可以被劃分為不超過logN個區間,我們用線段樹暴力對這每個區間進行維護,就完成了對一條路徑的操作
那麼單次複雜度顯然是log^2N

問題來了,怎麼劃分呢?

注意到,一棵樹的dfs序是不唯一的,因為在dfs中,當我們從當前節點往下遞迴時,我們可以選擇任意一個子節點進行dfs

而我們的“特殊的劃分方式”就是往下遞迴時先dfs當前節點的重兒子(即size最大的兒子),於是就做完了……

“啊,這麼容易就把路徑給劃分成不超過logN條鏈?”

證明:首先,這樣劃分使每條重鏈其節點在dfs序上都是連續的一段,於是只需證明每條路徑在樹上最多會經過不超過logN條鏈

考慮一個點到根節點,最多需要經過幾條輕邊(每經過一條輕邊,意味著經過鏈數+1;若為重邊,鏈數不變)

假設我們用size表示當前節點的子樹大小

那麼一旦經過一條輕邊跳到其父節點,其size至少乘2,那麼到根節點,其size最多有log_2 N次乘2的機會,所以一個點到根節點,最多需要經過log_2 N條輕邊

因此一條路徑的兩個端點在向上跳到lca的過程中需要經過的輕邊數一定小於等於它們各自跳到根節點所經過的輕邊數,即小於等於2*log_N

(啊好吧好吧我說錯了,應該是最多不超過2*logN條,但這是常數不重要啦)

經過樹剖改進後,同一棵樹,我們只需對[6,6][1,4][9,9]三個區間進行操作

另外附上樹剖最壞的情況,即滿二叉樹(其實可以從1再伸出一條重鏈,使得路徑上的每條邊都是輕邊)

對了,還有一點要說的就是,劃分完以後怎麼跳
Ans:讓鏈頭深的先跳,直到跳到同一條鏈
Proof:
若兩點在同一條鏈上,lca即為深度小的點,否則鏈頭深度大的點的鏈頭一定小於它們的lca,讓它跳到鏈頭的父親,問題就又變成了求它鏈頭的父親與另一個點的lca,因此重複上述操作,最終可以剛好遍歷完整條路徑

剩下的就看程式碼吧

Code

#include<bits/stdc++.h>
using namespace std;
#define int long long
inline int read()
{
	register int x=0,w=1;
	register char ch=getchar();
	while((ch<'0'||ch>'9')&&ch!='-') ch=getchar();
	if(ch=='-') {w=-1;ch=getchar();	}
	while(ch>='0'&&ch<='9') {x=(x<<3)+(x<<1)+(ch^48);ch=getchar();	}
	return x*w;
}

const int M=1e5+10;
int o,u,v,k;
int n,m,root,p,cnt;
int val[M],s[M],fa[M],top[M],d[M],id[M],rk[M],son[M];
vector<int>vec[M];
//處理深度,父親,大小,重兒子 
struct node{
	int l,r,sum,tag;
}t[M<<2];
void pushup(int x)
{
	t[x].sum=t[x<<1].sum+t[x<<1|1].sum;
	t[x].sum%=p;
}
void pushdown(int x)
{
	int d=t[x].tag;
	t[x<<1].tag+=d;
	t[x<<1].tag%=p;
	t[x<<1|1].tag+=d;
	t[x<<1|1].tag%=p;
	t[x<<1].sum+=d*(t[x<<1].r-t[x<<1].l+1);
	t[x<<1].sum%=p; 
	t[x<<1|1].sum+=d*(t[x<<1|1].r-t[x<<1|1].l+1);
	t[x<<1|1].sum%=p; 
	t[x].tag=0;
}
void build(int x,int l,int r)
{
	t[x].l=l;t[x].r=r;
	if(l==r){
		t[x].sum=val[rk[l]];
		t[x].tag=0;
		return;
	}
	int mid=l+r>>1;
	build(x<<1,l,mid);build(x<<1|1,mid+1,r);
	pushup(x);
}
void add(int x,int l,int r,int z)
{
	if(t[x].l>=l&&t[x].r<=r){
		t[x].tag+=z;
		t[x].tag%=p;
		t[x].sum+=z*(t[x].r-t[x].l+1);
		t[x].sum%=p;
		return;
	}
	pushdown(x);
	int mid=t[x].l+t[x].r>>1;
	if(l<=mid) add(x<<1,l,r,z);
	if(r>mid) add(x<<1|1,l,r,z);
	pushup(x);
}
int ask(int x,int l,int r)
{
	if(t[x].l>=l&&t[x].r<=r){
		return t[x].sum;
	}
	pushdown(x);
	int res=0,mid=t[x].l+t[x].r>>1;
	if(l<=mid) res=ask(x<<1,l,r);
	if(r>mid) res+=ask(x<<1|1,l,r);
	return res%p;
}
//以上為線段樹,以下是樹剖
//第一遍dfs處理size,fa,deep,hson(重兒子)
void dfs1(int x)
{
	s[x]=1;int maxn=0;
	for(int i=0;i<vec[x].size();++i)
	{
		int y=vec[x][i];
		if(y==fa[x]) continue;
		fa[y]=x;
		d[y]=d[x]+1;
		dfs1(y);
		s[x]+=s[y];
		if(s[y]>maxn)
		{
			son[x]=y;
			maxn=s[y];
		}
	}
}
//第2遍dfs
void dfs2(int x)
{
	if(x==0) return;
	id[x]=++cnt;//id為x在dfs序中位置
	rk[cnt]=x;//rk為dfs序中第cnt位為哪個節點
	top[son[x]]=top[x];//top為該節點所在鏈鏈頭
        //先遍歷重兒子
	dfs2(son[x]);
	for(int i=0;i<vec[x].size();++i)
	{
		int y=vec[x][i];
		if(y==fa[x]||y==son[x]) continue;
		top[y]=y;
		dfs2(y);
	}
}
void add_path(int x,int y,int z)
{
	while(top[x]!=top[y])
	{
		if(d[top[x]]<d[top[y]]) swap(x,y);//讓鏈頭更深的先跳,一定不會跳超
		add(1,id[top[x]],id[x],z);
		x=fa[top[x]];
	}
	if(id[x]>id[y]) swap(x,y);
	add(1,id[x],id[y],k); //最終跳到同一條鏈
}
int ask_path(int x,int y)
{
	int res=0;
	while(top[x]!=top[y])
	{
		if(d[top[x]]<d[top[y]]) swap(x,y);
		res+=ask(1,id[top[x]],id[x]);
		res%=p;
		x=fa[top[x]];
	}
	if(id[x]>id[y]) swap(x,y);
	res+=ask(1,id[x],id[y]);
	res%=p;
	return res;
}//詢問操作與修改操作類似
void add_tree(int x,int z)
{
	add(1,id[x],id[x]+s[x]-1,z);
}
int ask_tree(int x)
{
	return ask(1,id[x],id[x]+s[x]-1);
}

signed main()
{
    n=read();m=read();
    root=read();p=read();
    for(int i=1;i<=n;++i) val[i]=read();
    for(int i=1;i<n;++i)
    {
    	u=read();v=read();
    	vec[u].push_back(v);
    	vec[v].push_back(u);
	}
	d[root]=1;
	dfs1(root);
	top[root]=root;
	dfs2(root);
	build(1,1,n);
	for(int i=1;i<=m;++i)
	{
		o=read();
		if(o==1)
		{
			u=read();v=read();k=read();
			add_path(u,v,k);
		}
		if(o==2)
		{
			u=read();v=read();
			cout<<ask_path(u,v)<<endl;
		}
		if(o==3)
		{
			u=read();k=read();
			add_tree(u,k);
		}
		if(o==4)
		{
			u=read();
			cout<<ask_tree(u)<<endl;
		}
	}
	return 0;
}
/*
5 20 1 100
5 6 7 2 3
1 3
1 5
3 4
3 2
1 2 5 -3
3 5 3
1 1 2 3
4 3

5 20 1 100
5 6 7 2 3
1 3
1 5
3 4
3 2
2 1 4
14
1 2 5 -3
4 1
11
3 5 3
1 1 2 3
2 3 4
9
4 3
18
*/