树链剖分详细解读(末尾附一些入门难度习题传送门)

不知大家发现没有,我们在做与树有关的题的时候,时常需要询问两点之间的路径上的一些信息(如最大点(边)权,点(边)权和),在没有修改才操作的时候,我们可以用诸如倍增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
也贴个博客吧:(如果没有说明我还没写完)

猜你喜欢

转载自blog.csdn.net/weixin_43790474/article/details/84556647
今日推荐