1. 程式人生 > >樹鏈剖分詳細解讀(末尾附一些入門難度習題傳送門)

樹鏈剖分詳細解讀(末尾附一些入門難度習題傳送門)

不知大家發現沒有,我們在做與樹有關的題的時候,時常需要詢問兩點之間的路徑上的一些資訊(如最大點(邊)權,點(邊)權和),在沒有修改才操作的時候,我們可以用諸如倍增lca等方法維護兩點間路徑的資訊,而當出現修改操作的時候倍增等演算法就顯得無力。這時候,我們顯然需要一種資料結構來支援將樹上的路徑轉化為一段區間,然後用線段樹、樹狀陣列等方法維護樹上路徑資訊,那麼我們就需要用到樹鏈剖分了。

1.樹鏈剖分的思想

樹鏈剖分,正如它的名字,是將樹剖成一些互不相交的鏈,然後將一條鏈當做一個區間,再來維護這條鏈上的資訊。那麼將樹剖成怎樣的鏈,才能讓我們之後的一些操作複雜度最低呢?這有一種方法:一個節點u,定義sz[u]為以u為根的子樹節點數,那麼對於點u的sz[v]最大的子節點v,我們稱它為點u的重兒子,邊(u,v)稱為重邊,點u的其餘兒子為輕兒子,其它邊為輕邊。將一條全由重邊組成的邊叫做重鏈。那麼有性質 1.對於一條輕邊(u,v),sz[v] < sz[u] / 2
2.對於從根節點到某一點的路徑上,不超過log(n)條輕邊,不超過log(n)條重鏈。
這樣,我們顯然可以發現,我們只需維護重鏈,對於輕邊暴力處理即可達到nlogn的複雜度了。
(現在你可能有些迷茫,但相信你看了之後的實現後會對這種方法有更深入的理解)

2.樹鏈剖分的實現

首先我們需要處理一棵樹上的重鏈,重兒子等資訊,這裡我們用兩個dfs來實現。
(一些陣列名字的解釋:sz[u]:以u為根的子樹節點數,dep[u]:u點的深度,fa[u]:u點的父親節點,son[u]:u點的重兒子,top[u]:u點所處重鏈的頂端,tid[u]:u點的dfs序(保證每條重鏈上的點編號是連續的,方便用資料結構維護))
第一個dfs:

void dfs1(int u , int w)
{
	sz[u] = 1;
	path[u] = w;//記錄u到它父親的邊權,在題目中維護的是邊權而非點權時要用到
	for (int i = head[u]; ~i; i = E[i].next)
	{
		int v = E[i].v;
		if (v != fa[u])
		{
			dep[v] = dep[u] + 1;
			fa[v] = u;
			dfs1(v ,  E[i].w);
			sz[u] += sz[v];
			if (son[u] == -1 || sz[v] > sz[son[u]])
			{
				son[u] = v;//找重兒子
			}
		}
	}
}

第二個dfs:

void dfs2(int u , int Top)
{
	top[u] = Top;
	tid[u] = ++cnt;//當前點的編號
	if (son[u] != -1)
	{
		dfs2(son[u] , Top);//先搜尋重兒子,以保證重鏈編號連續,方便區間操作
	}
	for (int i = head[u]; ~i; i = E[i].next)
	{
		int v = E[i].v;
		if (v != son[u] && v != fa[u])
		{
			dfs2(v , v);//v是u的輕兒子,故v為所在重鏈鏈首。
		}
	}
}

完成了對樹結構的剖分,我們就要利用重鏈編號連續這一重要性質來解題啦!
首先我們考慮維護一條路徑(x -> y)上的點權:
我們用一個線段樹最底層(l == r)維護編號為l(tid[u] == l)的點權,向上更新就與普通線段樹沒有區別。
第一種情況:x和y在同一條重鏈上,顯然,x到y是一段連續的區間,那麼我們直接線上段樹相區間(tid[x] -> tid[y])上操作就完事了。
第二種情況:x和y不在同一條重鏈上,那麼,我們應該想到,將其分為(x->lca)(y->lca)兩部分來處理。這時我們就可以順著重鏈往上跳,但要注意,不是x和y一起跳,而是將top[x]與top[y]中深度小的先跳(防止跳後錯開),直到top[x]=top[y],但由於我們是分開跳,只有一個節點跳到了lca,需將另一點也跳到lca,那麼就有如下實現:

void update(int id , int l , int r , int x , int y , int val)
{
	if (l >= x && r <= y)
	{
		tree[id] += (r - l + 1) * val;
		lazy[id] += val;
		return;//區間修改正常操作
	}
	int mid = (l + r) >> 1;
	pushdown(id , l , r , mid);
	if (mid >= x)
	{
		update(id << 1 , l , mid , x , y , val);
	}
	if (mid < y)
	{
		update(id << 1 | 1 , mid + 1 , r , x , y , val);
	}
	tree[id] = tree[id << 1] + tree[id << 1 | 1];
}
void update_path(int x , int y , int val)
{
	while (top[x] != top[y])
	{
		if (dep[top[x]] < dep[top[y]])//讓top[]小的先跳
		{
			swap(x , y);
		}
		update(1 , 1 , n , tid[top[x]] , tid[x] , val);
		x = fa[top[x]];//跳到鏈首的父親節點
	}
	if (dep[x] > dep[y])//將沒跳完的一段補上
	{
		swap(x , y);
	}
	update(1 , 1 , n , tid[x] , tid[y] , val);
}

查詢也一樣:

long long query(int id , int l , int r , int x , int y)
{
	if (l >= x && r <= y)
	{
		return tree[id];
	}
	int mid = (l + r) >> 1;
	pushdown(id , l , r , mid);
	long long res = 0;
	if (mid >= x)
	{
		res += query(id << 1 , l , mid , x , y);
	}
	if (mid < y)
	{
		res += query(id << 1 | 1 , mid + 1 , r , x , y);
	}
	return res;
}
long long query_path(int x , int y)
{
	long long ans = 0;
	while (top[x] != top[y])
	{
		if (dep[top[x]] < dep[top[y]])
		{
			swap(x , y);
		}
		ans += query(1 , 1 , n , tid[top[x]] , tid[x]);
		x = fa[top[x]];
	}
	if (dep[x] > dep[y])
	{
		swap(x , y);
	}
	ans += query(1 , 1 , n , tid[x] , tid[y]);
	return ans;
}

那麼點權就講完了,說說邊權:對於邊權只與點權有幾處不同
1.線段樹底層維護當前點到其父親節點的邊權(tree[id] = path[u] , (tid[u] = l))。
2.對於更新合成和查詢時將update(或query)(1 , 1 , n , tid[x] , tid[y] , val)
改為update(1 , 1 , n , tid[son[x]] , tid[y] , val)。(易證)
然後其他都是一樣的。
最後附上一個板子:

#include<bits/stdc++.h>
using namespace std;
const int maxn = 100010;
struct edge
{
	int v , w , next;
}E[maxn * 2];
int len , head[maxn];
void add(int u , int v , int w)
{
	E[len].v = v , E[len].w = w , E[len].next = head[u];
	head[u] = len++;
}
int son[maxn] , fa[maxn] , sz[maxn] , top[maxn] , rnk[maxn] , tid[maxn] , dep[maxn] , path[maxn];
int n , q;
void dfs1(int u , int w)
{
	sz[u] = 1;
	path[u] = w;//記錄u到它父親的邊權,在題目中維護的是邊權而非點權時要用到
	for (int i = head[u]; ~i; i = E[i].next)
	{
		int v = E[i].v;
		if (v != fa[u])
		{
			dep[v] = dep[u] + 1;
			fa[v] = u;
			dfs1(v ,  E[i].w);
			sz[u] += sz[v];
			if (son[u] == -1 || sz[v] > sz[son[u]])
			{
				son[u] = v;//找重兒子
			}
		}
	}
}
int cnt;
void dfs2(int u , int Top)
{
	top[u] = Top;
	tid[u] = ++cnt;//當前點的編號
	if (son[u] != -1)
	{
		dfs2(son[u] , Top);//先搜尋重兒子,以保證重鏈編號連續,方便區間操作
	}
	for (int i = head[u]; ~i; i = E[i].next)
	{
		int v = E[i].v;
		if (v != son[u] && v != fa[u])
		{
			dfs2(v , v);//v是u的輕兒子,故v為所在重鏈鏈首。
		}
	}
}
long long tree[4 * maxn] , lazy[4 * maxn];
void pushdown(int id , int l , int r , int mid)
{
	if (lazy[id])
	{
		lazy[id << 1] += lazy[id];
		lazy[id << 1 | 1] += lazy[id];
		tree[id << 1] += lazy[id] * (mid - l + 1);
		tree[id << 1 | 1] += lazy[id] * (r - mid);
		lazy[id] = 0;
	}
}
void update(int id , int l , int r , int x , int y , int val)
{
	if (l >= x && r <= y)
	{
		tree[id] += (r - l + 1) * val;
		lazy[id] += val;
		return;//區間修改正常操作
	}
	int mid = (l + r) >> 1;
	pushdown(id , l , r , mid);
	if (mid >= x)
	{
		update(id << 1 , l , mid , x , y , val);
	}
	if (mid < y)
	{
		update(id << 1 | 1 , mid + 1 , r , x , y , val);
	}
	tree[id] = tree[id << 1] + tree[id << 1 | 1];
}
void update_path(int x , int y , int val)
{
	while (top[x] != top[y])
	{
		if (dep[top[x]] < dep[top[y]])//讓top[]小的先跳
		{
			swap(x , y);
		}
		update(1 , 1 , n , tid[top[x]] , tid[x] , val);
		x = fa[top[x]];//跳到鏈首的父親節點
	}
	if (dep[x] > dep[y])//將沒跳完的一段補上
	{
		swap(x , y);
	}
	update(1 , 1 , n , tid[son[x]] , tid[y] , val);//邊權
	//update(1 , 1 , n , tid[x] , tid[y] , val);//點權
}
long long query(int id , int l , int r , int x , int y)
{
	if (l >= x && r <= y)
	{
		return tree[id];
	}
	int mid = (l + r) >> 1;
	pushdown(id , l , r , mid);
	long long res = 0;
	if (mid >= x)
	{
		res += query(id << 1 , l , mid , x , y);
	}
	if (mid < y)
	{
		res += query(id << 1 | 1 , mid + 1 , r , x , y);
	}
	return res;
}
long long query_path(int x , int y)
{
	long long ans = 0;
	while (top[x] != top[y])
	{
		if (dep[top[x]] < dep[top[y]])
		{
			swap(x , y);
		}
		ans += query(1 , 1 , n , tid[top[x]] , tid[x]);
		x = fa[top[x]];
	}
	if (dep[x] > dep[y])
	{
		swap(x , y);
	}
	ans += query(1 , 1 , n , tid[son[x]] , tid[y]);//邊權
	//ans += query(1 , 1 , n , tid[x] , tid[y]);//點權
	return ans;
}
int main()
{
	memset(head , -1 , sizeof(head));
	memset(son , -1 , sizeof(son));
	scanf("%d%d" , &n , &q);
	for (int i = 1; i < n; i++)
	{
		int u , v;
		long long w;
		scanf("%d%d%lld" , &u , &v , &w);
		add(u , v , w);
		add(v , u , w);
	}
	dfs1(1 , 0);
	dfs2(1 , 1);
	for (int i = 1; i <= n; i++)
	{
		update(1 , 1 , n , tid[i] , tid[i] , path[i]);//邊權
		//update(1 , 1 , n , tid[i] , num[i])//點權
	}
	for (int i = 1; i <= q; i++)
	{
		int judge;
		scanf("%d" , &judge);
		if (judge == 1)
		{
			int x , y;
			long long val;
			scanf("%d%d%lld" , &x , &y , &val);
			update_path(x , y , val);
		}
		else
		{
			int x , y;
			scanf("%d%d" , &x , &y);
			printf("%lld\n" , query_path(x , y));
		}
	}
	return 0;
}

對了,補充一句,對於整棵子樹整體修改查詢,這個不用樹剖都可以做到,直接dfs維護就行了。(x的子樹對應的區間:tid[x] ~ tid[x] + sz[x] - 1)
好長啊,如果你看到這還沒跑,說明我寫的還是不錯的,那就點個贊再走唄(5000多字,還是萌新的我敲得頭皮發麻,不過嘛,我們老師給我佈置這個任務,說以後可能有學弟學妹會來看,瞬間就有了動力?233)。

一些習題(都在洛谷上,bzoj介面看著難受)

1.板子:https://www.luogu.org/problemnew/show/P3384
2.換了個樣子的板子:https://www.luogu.org/problemnew/show/P4315
3.改了一點的板子:https://www.luogu.org/problemnew/show/P2486
4.表面樹剖,其實直接倍增無壓力:http://172.25.37.251/problem/142 (學校內網)
5.進階(有難度,慎選):[FJOI2014]最短路 bzoj3694(有一個很像的別找錯了)
順便貼個自己寫的這道題的部落格(有興趣的可以看看):(如果沒有說明我還沒寫完)
6.進階2(非常複雜,慎選,我也沒過):[ZJOI2011]道館之戰 bzoj2325
https://www.luogu.org/problemnew/show/P4679
7.進階3(需要動態開點的知識,慎選):[SDOI2014]旅行 bzoj3531
https://www.luogu.org/problemnew/show/P3313
也貼個部落格吧:(如果沒有說明我還沒寫完)