树链剖分简述+洛谷P3384模板

为什么我们需要树链剖分

大家都知道用线段树对一连续区间进行整体操作,如:将区间[l,r]的值增加x,或者求[l,r]的值的和。但是当我们将操作的对象由连续的区间变为树,这时候就不是很方便了。

不过,还有一种大家比较熟悉的,用于树上对某一子树进行整体操作的方法,那便是用dfs序构建线段树,这个方法的好处在于,当我们访问一棵子树的时候,我们将连续地访问完其所有的子节点,使得从根结点 x 到遍历完整个树的过程中的形成dfs序是连续的,此时,我们利用dfs序构建线段树,就可以很好的对树上某个子树进行整体操作了。

但是,普通的dfs序构建的线段树还是有所欠缺,比如我们要修改树上任意两点的最短路上所有的结点,显然,普通的dfs序不够用了,因此,我们需要引入树链剖分的方法求dfs序并构建线段树。


树链剖分基本术语

重儿子:最大的子树,也就是节点个数最多的子树

轻儿子:不是重儿子的子树

重边:由当前结点和重儿子之间的边

轻边:由当前结点和轻儿子之间的边

重链:所有的重边的集合

轻边:所有轻边的集合

链头:每条重链或轻链中深度最小的点

为了帮助理解,我们画一个图来说明一下这些术语,红边代表重边,蓝边代表轻边,红圈代表链头


树链剖分原理

在上图中,我们把树分成数个轻链和重链,这个是重要的,因为相比于普通的dfs序构建线段树,此时我们的dfs序不仅保留了某子树中所有结点的dfs序连续,而且我们也将保证同一条链上的点的dfs也是连续的(具体实现请看代码,这里说明思路)

此时我们就会发现一个方便之处,假设此时我们需要执行操作:将s和e之间的结点的权值增加x,设想,如果s和e处于同一条链上的时候,由于同一条链上的点的dfs连续,那么我们就可以直接利用由dfs序构建的线段树来更新s和e之间的结点了,因为此时s和e之间的结点相当于线段树上一段连续的区间。

那么,如果s和e不在同一条链上呢?假设此时的s = 7,e = 12(见上面的图),此时s和e不在同一条链上,通过观察我们可以知道s->e 的路径为: 7 - 5 - 4 -3- 2 -11 - 12 ,显然,我们可以将这个路径分为三段: 7 , 5 ~2 , 11 ~12 ,同时,这三段路径分别处于三条链上,由之前的推导,我们知道对这三段路径进行操作是比较容易的,而且我们发现,对这三段路径上的点进行操作,相当于对 7~12这条路径上的点进行操作,两者效果等价,这便是树链剖分的特点了。

树链剖分的特点:将两点之间dfs不连续的路径分隔为数个dfs序连续的路径,对这数个dfs连续的路径进行相当于对线段树区间的操作,并且此操作的效果等价于对两点之间所有结点的操作


代码区(P3384,树链剖分模板题)

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<string>
#include<cmath>
#include<fstream>
#include<vector>
#include<stack>
#include <map>
#include <iomanip> 
#define bug cout << "**********" << endl
#define out cout<<"---------------"<<endl
#define show(x, y) "["<<x<<","<<y<<"] "
//#define LOCAL = 1;
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
//const int mod = 1e8;
const int Max = 1e5 + 10;

struct Tree
{
	int deep, father, val;		//结点深度,父节点,权值
	int size, heavy;			//以此结点为根节点的树的大小。重儿子的编号
	int id, top;				//对应于线段树上的编号(dfs序),链头编号
}tree[Max];						//原本树的结点信息

int n, m, root, mod;
int toTree[Max], cnt;			//记录线段树上某一点对应的树的编号,也就是根据dfs序找到对应的树

//以下为线段树操作
struct Node
{
	int l, r;
	int sum, lazy;
}node[Max<<2];

void build(int l, int r, int num)
{
	node[num].l = l;
	node[num].r = r;
	node[num].lazy = 0;
	if (l == r)
	{
		node[num].sum = tree[toTree[l]].val;
		return;
	}
	int mid = (l + r) >> 1;
	build(l, mid, num << 1);
	build(mid + 1, r, num << 1 | 1);
	node[num].sum = node[num << 1].sum + node[num << 1 | 1].sum;
}

void push_down(int num)
{
	if (node[num].lazy != 0)
	{
		int &lazy = node[num].lazy;

		node[num << 1].sum += (node[num << 1].r - node[num << 1].l + 1) * lazy;
		node[num << 1].sum %= mod;

		node[num << 1 | 1].sum += (node[num << 1 | 1].r - node[num << 1 | 1].l + 1) * lazy;
		node[num << 1 | 1].sum %= mod;

		node[num << 1].lazy += lazy;
		node[num << 1 | 1].lazy += lazy;

		node[num << 1].lazy %= mod;
		node[num << 1 | 1].lazy %= mod;

		lazy = 0;
	}
}

void upData(int l, int r, int val, int num)	//更新区间
{
	if (l <= node[num].l && node[num].r <= r)
	{
		node[num].sum += (node[num].r - node[num].l + 1)*val;node[num].sum %= mod;
		node[num].lazy += val;node[num].lazy %= mod;
		return;
	}
	push_down(num);
	int mid = (node[num].l + node[num].r) >> 1;

	if (l <= mid)
		upData(l, r, val, num << 1);
	if (r > mid)
		upData(l, r, val, num << 1 | 1);
	node[num].sum = node[num << 1].sum + node[num << 1 | 1].sum;
}

int query(int l, int r, int num)	//查询区间和
{
	if (l <= node[num].l && node[num].r <= r)
		return node[num].sum%mod;

	push_down(num);

	int mid = (node[num].l + node[num].r) >> 1;
	int ans = 0;
	if (l <= mid)
		ans += query(l, r, num << 1);
	if (r > mid)
		ans += query(l, r, num << 1 | 1);

	return ans % mod;
}

//以下为树链剖分的部分
struct Edge
{
	int to, next;
}edge[Max<<1];			//边

int head[Max], tot;
int val[Max];			//记录各个结点的权值

void init()
{
	memset(head, -1, sizeof(head));tot = 0;
	memset(toTree, -1, sizeof(toTree));cnt = 0;
}

void add(int u, int v)
{
	edge[tot].to = v;
	edge[tot].next = head[u];
	head[u] = tot++;
}

void dfs1(int now, int fa, int deep)
{
	tree[now].val = val[now];
	tree[now].father = fa;
	tree[now].deep = deep;
	tree[now].size = 1;
	tree[now].heavy = -1;			//初始化为无重边(也就是看作叶子节点了)
	int max_son = -1;
	for (int i = head[now]; i != -1; i = edge[i].next)
	{
		int v = edge[i].to;
		if (v == fa) continue;

		dfs1(v, now, deep + 1);

		tree[now].size += tree[v].size;
		if (tree[v].size > max_son)	//更新重边
			tree[now].heavy = v, max_son = tree[v].size;
	}
}

void dfs2(int now, int top)			//top为当前链的链头
{
	tree[now].top = top;
	tree[now].id = ++cnt;
	toTree[cnt] = now;
	if (tree[now].heavy == -1) return;						//叶子结点
	dfs2(tree[now].heavy, top);								//先处理重链
	for (int i = head[now]; i != -1; i = edge[i].next)		//处理轻链
	{
		int v = edge[i].to;
		if (v == tree[now].father || v == tree[now].heavy) continue;
		dfs2(v, v);											//此时v为一条轻链的链头(画一下就知道为什么了)
	}
}

void upData2(int s, int e, int val)							//更新s->e上的结点,将dfs不连续的路径分为数个dfs序连续的路径,对这个数个dfs序连续的路径进行操作
{
	while (tree[s].top != tree[e].top)						//不断地将深度大的上移,使得最后两个点都在同一个链上
	{
		if (tree[tree[s].top].deep < tree[tree[e].top].deep) swap(s, e);
		upData(tree[tree[s].top].id, tree[s].id, val, 1);	//同时更新分出来的dfs序连续的路径
		s = tree[tree[s].top].father;
	}
	if (tree[s].deep > tree[e].deep) swap(s, e);
	upData(tree[s].id, tree[e].id, val, 1);
}

int query2(int s, int e)	//思路和upData2的一样:将dfs不连续的路径分为数个dfs序连续的路径,对这个数个dfs序连续的路径进行操作
{
	int sum = 0;
	while (tree[s].top != tree[e].top)
	{
		if (tree[tree[s].top].deep < tree[tree[e].top].deep) swap(s, e);
		sum += query(tree[tree[s].top].id, tree[s].id, 1);
		sum %= mod;
		s = tree[tree[s].top].father;
	}
	if (tree[s].deep > tree[e].deep) swap(s, e);
	sum += query(tree[s].id, tree[e].id, 1);
	sum %= mod;
	return sum;
}


int main()
{
#ifdef LOCAL
	freopen("input.txt", "r", stdin);
	freopen("output.txt", "w", stdout);
#endif
	while (scanf("%d%d%d%d", &n, &m, &root, &mod) != EOF)
	{
		init();
		for (int i = 1;i <= n;i++)
			scanf("%d", val + i);
		for (int i = 1, u, v;i < n;i++)
		{
			scanf("%d%d", &u, &v);
			add(u, v);add(v, u);
		}
		dfs1(root, -1, 0);		//先预处理出每个结点的基本信息,比如每个根结点对应的重边,以为构建链做准备
		dfs2(root, root);		//根据之前已经求出的每个结点的信息,获得链上结点dfs连续的dfs序
		build(1, n, 1);			//根据之前得到的dfs序构建线段树
		while (m--)
		{
			int type, x, y, z;
			scanf("%d", &type);
			if (type == 1)
				scanf("%d%d%d", &x, &y, &z), upData2(x, y, z);			//将不连续的dfs划分,使得对数个连续的dfs序路径进行操作,以达所需效果
			else if (type == 2)
				scanf("%d%d", &x, &y), printf("%d\n", query2(x, y));	//将不连续的dfs划分,使得对数个连续的dfs序路径进行操作,以达所需效果
			else if (type == 3)
				scanf("%d%d", &x, &z), upData(tree[x].id, tree[x].id + tree[x].size - 1, z,1);			//同一子树中的点,其dfs序是连续的,直接操作即可
			else
				scanf("%d", &x), printf("%d\n", query(tree[x].id, tree[x].id + tree[x].size - 1,1));	//同一子树中的点,其dfs序是连续的
		}
	}
	return 0;
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值