樹鏈剖分入門詳解

先拋出一個問題,一棵n個點的樹,每個點有一個不變的權值,m次詢問任意兩點之間的權值和。

最簡單的算法:m次循環,每次從一個點出發,dfs累計走過的路徑,直到到達另外一個點。

int dfs(int x,int fa,long long deep)
{
	d[x]=deep;
	for(int i=head[x];i;i=nxt[i])
	{
		int u=to[i];if(u==fa) continue;
		dfs(u,x,deep+w[i]); 
	}
}

如果你會前綴和,將其利用到樹上,將一條條鏈想象成數組,於是乎我們就可以快速求出在一條鏈上的兩點之間的權值和。

對於不在一條鏈上的點怎麼辦呢?我們求一下兩點(a,b)的LCA,將(a,b)分爲(lca,a)和(lca,b),這樣lca和每一個點都在一條鏈上了。

void get_treesum(int x,int fa)
{
	sum[x]=sum[fa]+a[x];
	for(int i=head[x];i;i=nxt[i])
	{
		int u=to[i];if(u==fa) continue;
		get_treesum(u,x);
	}
}
long long get_ans(int x,int y,int lca)
{
	return sum[x]+sum[y]-sum[lca]-sum[fa[lca]];
}

如果還需要你支持一種操作:修改任意一個點的權值

我們還是先考慮處理的是數組,對於數組,我們不能再使用前綴和了,否則每次修改都是o(n)的,我們就可以使用線段樹或者樹狀數組來維護這個數組,做到查詢和修改都是o(logn)。

那對於樹,可以把樹看成很多數組組成的,那麼我們可以直接套用線段樹麼?

顯然不可以的,因爲要用多棵線段樹維護,那麼單次修改的複雜度可能高達o(nlogn)。

那麼可否用一棵線段樹進行維護呢?我們姑且隨便編一下號。id[x]表示x點在線段樹上的位置。

對於修改o(logn),但是查詢需要一個個點上去搜o(nlogn)。

顯然線段樹上單點操作和區間操作的複雜度是相同的,而我們每次操作或者查詢的點的數量是一定的。

所以我們需要儘量多的區間操作,儘量少的單點操作使我們的總複雜度最小。

那麼我們對於一條鏈的處理,最簡單的就是從上到下id[x]依次增加,那麼我們處理這條鏈的時候只需要一次區間操作。

但是問題是我們有很多鏈的處理,不可能滿足每一條鏈自上而下的id都是依次增加的,只能挑選部分的鍊形成連續的id。

那我們肯定儘量挑選連續鏈   儘量長一些,這樣造成區間處理的點可以更多,

比如說挑選的這條鏈從根節點出發延申,顯然可以延申到深度最大的葉節點結果更優。

那麼我們每次dfs到一個點,就將它節點數最大的兒子標記爲重兒子。

void dfs1(int x,int fa)
{
	son[x]=1;dep[x]=dep[fa]+1;
	for(int i=head[x];i;i=nxt[i])
	{
		int u=to[i];if(u==fa) continue;
		dfs1(u,x);son[x]+=son[u];
		if(son[u]>son[wson[x]]) wson[x]=son[u];
	}
}

 在第二次dfs的時候,我們主要進行兩個操作,一個是將x(dfs到的點)給一個線段樹上的位置編號(該編號滿足在dfs2完之後很多鏈的編號是連續的,實現線段樹上儘可能多的區間處理),另一個操作是維護每個點的鏈首是誰。

鏈首是什麼?就是你要處理形成的鏈的頂端(暫時不理解也沒關係)。

void dfs2(int x,int topp)
{
	topf[x]=topp;id[x]=++cnt;
	if(wson[x]) dfs2(wson[x],topp);
	for(int i=head[x];i;i=nxt[i])
	{
		int u=to[i];if(u==wson[x]||u==fa[x]) continue;
		dfs2(u,u);
	}
}

處理完之後可以得到類似下圖,藍字是id[],紅點表示鏈首,粗線表示重鏈,細線表示輕鏈。

這樣我們任意取一個點到根節點,可以發現處理次數均不大於3次。

下面給出從x到根節點最多經過2*logn條鏈的簡單證明:

1.假設上方全部是輕鏈,那麼假設當前走過一條輕鏈u→v(fa[u]==v),必定存在v的一個兒子的節點數大於son[u],那麼走過這條輕鏈的時候,我們就已經至少有2*son[u]個點不需要操作了,每跳一次輕鏈就少處理當前兒子數的兩倍,所以最多處理logn次。

2.假設上方全是重鏈,只需要一次線段樹操作。

3.假設又有重鏈又有輕鏈,那麼我們處理的不是重鏈就是輕鏈,處理完一次重鏈上面的鏈必定是輕鏈(如果重鏈上面是重鏈,那麼他們顯然可以連接在一起處理一次),所以走重鏈的次數和約等於走輕鏈的次數,那麼最糟糕的情況就是走logn次輕鏈和logn次重鏈。

樹鏈剖分非常依賴線段樹的操作,線段樹用的越好,鏈剖用的越好,下面是建線段樹的操作。

#define lson o<<1
#define rson (o<<1)|1
void build_tree(int o,int l,int r)
{
	if(l==r) {t[o]=a[l];return ;}
	int mid=(l+r)/2;
	build_tree(lson,l,mid);build_tree(rson,mid+1,r); 
}

t[o]中的o對應的是線段樹的節點編號,而a[l]的l代表線段樹的位置編號,所以我們需要在dfs2中加入下列操作:

    topf[x]=topp;id[x]=++cnt;a[cnt]=v[x];

v[x]對應的是真實樹上的x點的權值,x在線段樹上的位置編號是cnt,所以a[]是過渡數組,將線段樹節點編號和點權值相聯繫。

下面是修改真實樹x節點的權值改爲y(實際上就是線段樹的單點修改):

void c_tree(int o,int l,int r,int pos,int val)
{
	if(l==r) {t[o]=val;return ;}
	int mid=(l+r)/2;
	if(pos<=mid) c_tree(lson,l,mid,pos,val);
	else c_tree(rson,mid+1,r,pos,val);
	t[o]=t[lson]+t[rson]; 
}
	c_tree(1,1,n,id[x],y);

下面是我們查詢一條鏈權值和的操作,因爲是一條處理後的鏈,所以id是連續的(l,l+1,l+2......r-2,r-1,r),下面的查詢操作:

LL quiry_tree(int o,int l,int r,int ll,int rr)
{
	if(ll<=l&&rr>=r) return t[o];
	int mid=(l+r)/2;LL ans=0;
	if(ll<=mid) ans+=quiry_tree(lson,l,mid,ll,rr);
	if(rr>mid) ans+=quiry_tree(rson,mid+1,r,ll,rr);
	return ans;
}

那麼我們現在處理兩個點之間的權值和,我們已經可以處理一條鏈的權值和,我們現在只需要把兩點之間的線段分成很多條鏈,我們兩個點同時處理,每次先把 鏈首 深度 大的鏈先處理,然後該點跳到鏈首的父親位置(點的位置都是沒有處理的點),直到他們都跳到同一條鏈中(請想象),再處理這條鏈即可。

LL quiry_road(int x,int y)
{
	LL ans=0;
	while(topf[x]!=topf[y])        //不是同一條鏈就跳
	{
		if(dep[topf[x]]<dep[topf[y]]) swap(x,y);    //每次處理鏈首深度大的
		ans+=quiry_tree(1,1,n,id[topf[x]],id[x]);
		x=fa[topf[x]];
	}
	if(dep[x]<dep[y]) swap(x,y);
	ans+=quiry_tree(1,1,n,id[y],id[x]);    
	return ans;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章