树链剖分学习笔记

 


怕到时候忘了,来写一篇笔记

前置芝士:树的存储与遍历,dfs 序,线段树。

树链剖分的思想及能解决的问题:#

树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息。

具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。

树链剖分(树剖/链剖)有多种形式,如 重链剖分,长链剖分 和用于 Link/cut Tree 的剖分(有时被称作“实链剖分”),大多数情况下(没有特别说明时),“树链剖分”都指“重链剖分”。

重链剖分可以将树上的任意一条路径划分成不超过 O(logn) 条连续的链,每条链上的点深度互不相同(即是自底向上的一条链,链上所有点的 LCA 为链的一个端点)。

重链剖分还能保证划分出的每条链上的节点 DFS 序连续,因此可以方便地用一些维护序列的数据结构(如线段树)来维护树上路径的信息。

重链剖分#

我们给出一些定义:

定义 重子节点 表示其子节点中子树最大的子结点。如果有多个子树最大的子结点,取其一。如果没有子节点,就无重子节点。

定义:轻子节点表示剩余的所有子结点。

从这个结点到重子节点的边为重边。

到其他轻子节点的边为轻边。

若干条首尾衔接的重边构成重链。

把落单的结点也当作重链,那么整棵树就被剖分成若干条重链。

以上来自Oi-wiki

树链剖分的实现#

以洛谷的模板题为例。

先给出以下定义:

fa[u] 表示u节点的父节点。

dep[u] 表示u节点的深度。

top[u] 表示u节点所在链的顶端。

son[u] 表示u节点的重儿子v的编号。

siz[u] 表示以u为根的子树的大小。

id[u] 表示u在树链剖分的dfs序。

树剖的实现过程分为:

dfs1:#

dfs1中我们要处理出fa,dep,siz,son数组。

代码实现如下:

void dfs1(int u, int fath)
{
	fa[u] = fath, dep[u] = dep[fath] + 1;
	siz[u] = 1;
	for(int i = h[u]; i; i = nxt[i])
	{
		int v = to[i];
		if(v == fath) continue;
		dfs1(v, u);
		siz[u] += siz[v];
		if(siz[v] > siz[son[u]]) son[u] = v;
	}
}

dfs2:#

dfs2中我们遵循优先走重边的原则,处理出我们的dfs序,也就是处理出top,id数组。

代码实现如下:

void dfs2(int u, int fst)  //处理top,id,fst表示当前链的顶端
{
	id[u] = ++ idx;
	a[idx] = w[u]; //a数组表示节点u的权值在线段树中的位置权值
	top[u] = fst;
	if(!son[u]) return; //没有重儿子意味着没有子节点
	dfs2(son[u], fst);  //跟重儿子一条链
	for(int i = h[u]; i; i = nxt[i])
	{
		int v = to[i];
		if(v == fa[u] || v == son[u]) continue; //轻儿子既不是父节点也不是重儿子
		dfs2(v, v);  //以自己为链顶重新开一条链
	}
}

树剖求解LCA(最近公共祖先):#

思路:不断的将两个节点中所在链的链顶深度深的节点往上跳,知道两个节点所在链相同,即top[u]==top[v],此时两个节点中深度较低的节点即为LCA

代码如下:

int lca(int u, int v)
{
	while(top[u] != top[v])
	{
		if(dep[top[u]] < dep[top[v]]) swap(u, v);
		u = fa[top[u]];
	}
	if(dep[u] > dep[v]) swap(u, v);
	return u; 
}

操作一:将树从 x 到结点 y 最短路径上所有节点的值都加上z#

这个时候就要祭出我们的线段树了,在dfs2中我们采取了优先走重儿子的原则,所以在一条链中的dfs序一定是连续的,我们能够根据这一特点快速的在线段树中修改某一段的权值。

思路与求解LCA基本相同,代码:

void path_update(int u, int v, int w)
{
	w %= p;
	while(top[u] != top[v])
	{
		if(dep[top[u]] < dep[top[v]]) swap(u, v);
		update(id[top[u]], id[u], 1, w);
		u = fa[top[u]];
	}
	if(dep[u] > dep[v]) swap(u, v);
	update(id[u], id[v], 1, w);
}

操作二:求树从 xy 结点最短路径上所有节点的值之和#

理解了LCA和树上修改权值后,查询其实也不难了,只要将操作一中的修改操作改为查询操作即可,代码如下:

int path_query(int u, int v)
{
	int res = 0;
	while(top[u] != top[v])
	{
		if(dep[top[u]] < dep[top[v]]) swap(u, v);
		res = (res + query(id[top[u]], id[u], 1)) % p;
		u = fa[top[u]];
	}
	if(dep[u] > dep[v]) swap(u, v);
	res = (res + query(id[u], id[v], 1)) % p;
	return res;
}

操作三:将以 x 为根节点的子树内所有节点值都加上 z#

因为我们在对树进行剖分的过程中使用的是 dfs ,所以每一颗子树内的 dfs 序也是连续的,利用我们之前处理出来的 siz 数组就能轻松解决这个问题。

代码如下:

void son_update(int u, int w)
{
	w %= p;
	update(id[u], id[u] + siz[u] - 1, 1, w);
}

操作四:求以 x 为根节点的子树内所有节点值之和#

跟操作三基本一样,这里不多赘述,直接放代码:

int son_query(int u)
{
	return query(id[u], id[u] + siz[u] - 1, 1) % p;
}

完整代码#

#include <bits/stdc++.h>
#define L(i, j, k) for(int i = (j); i <= (k); i ++)
#define R(i, j, k) for(int i = (j); i >= (k); i --)
using namespace std;
typedef long long LL;
const int N = 1e5 + 5, INF = 0x3f3f3f3f;
const LL mod = 1e9 + 7;
int n, m, r, p;
int w[N], a[N];
int h[N], nxt[N << 1], to[N << 1], cnt;
int id[N], top[N], fa[N], dep[N], siz[N], son[N], idx;
struct segment_tree
{
	int l, r, val, tag;
	#define l(x) tr[x].l
	#define r(x) tr[x].r
	#define val(x) tr[x].val
	#define tag(x) tr[x].tag
} tr[N << 2];
void pushup(int x)
{
	val(x) = (val(x << 1) + val(x << 1 | 1)) % p;
}
void pushdown(int x)
{
	if(!tag(x)) return;
	tag(x << 1) = (tag(x) + tag(x << 1)) % p;
	tag(x << 1 | 1) = (tag(x) + tag(x << 1 | 1)) % p;
	val(x << 1) = (val(x << 1) + (r(x << 1) - l(x << 1) + 1) * tag(x) % p) % p;
	val(x << 1 | 1) = (val(x << 1 | 1) + (r(x << 1 | 1) - l(x << 1 | 1) + 1) * tag(x) % p) % p;
	tag(x) = 0;
}
void build(int l, int r, int x)
{
	l(x) = l, r(x) = r;
	if(l == r) 
	{
		val(x) = a[l];
		return;
	}
	int mid = l + r >> 1;
	build(l, mid, x << 1), build(mid + 1, r, x << 1 | 1);
	pushup(x);
}
void update(int l, int r, int x, int v)
{
	if(l <= l(x) && r(x) <= r)
	{
		val(x) = (val(x) + (r(x) - l(x) + 1) * v % p) % p;
		tag(x) = (tag(x) + v) % p;
		return;
	}
	pushdown(x);
	int mid = l(x) + r(x) >> 1;
	if(l <= mid) update(l, r, x << 1, v);
	if(r > mid) update(l, r, x << 1 | 1, v);
	pushup(x);
}
int query(int l, int r, int x)
{
	if(l <= l(x) && r(x) <= r)
		return val(x);
	pushdown(x);
	int mid = l(x) + r(x) >> 1, res = 0;
	if(l <= mid) res = (res + query(l, r, x << 1)) % p;
	if(r > mid) res = (res + query(l, r, x << 1 | 1)) % p;
	return res;
}
void add(int u, int v)
{
	to[++ cnt] = v, nxt[cnt] = h[u], h[u] = cnt;
}
void dfs1(int u, int fath)
{
	fa[u] = fath, dep[u] = dep[fath] + 1;
	siz[u] = 1;
	for(int i = h[u]; i; i = nxt[i])
	{
		int v = to[i];
		if(v == fath) continue;
		dfs1(v, u);
		siz[u] += siz[v];
		if(siz[v] > siz[son[u]]) son[u] = v;
	}
}
void dfs2(int u, int fst)
{
	top[u] = fst, id[u] = ++ idx, a[idx] = w[u];
	if(!son[u]) return;
	dfs2(son[u], fst);
	for(int i = h[u]; i; i = nxt[i])
	{
		int v = to[i];
		if(v == son[u] || v == fa[u]) continue;
		dfs2(v, v);
	}
}
void path_update(int u, int v, int w)
{
	w %= p;
	while(top[u] != top[v])
	{
		if(dep[top[u]] < dep[top[v]]) swap(u, v);
		update(id[top[u]], id[u], 1, w);
		u = fa[top[u]];
	}
	if(dep[u] > dep[v]) swap(u, v);
	update(id[u], id[v], 1, w);
}
int path_query(int u, int v)
{
	int res = 0;
	while(top[u] != top[v])
	{
		if(dep[top[u]] < dep[top[v]]) swap(u, v);
		res = (res + query(id[top[u]], id[u], 1)) % p;
		u = fa[top[u]];
	}
	if(dep[u] > dep[v]) swap(u, v);
	res = (res + query(id[u], id[v], 1)) % p;
	return res;
}
void son_update(int u, int w)
{
	w %= p;
	update(id[u], id[u] + siz[u] - 1, 1, w);
}
int son_query(int u)
{
	return query(id[u], id[u] + siz[u] - 1, 1) % p;
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m >> r >> p;
    L(i, 1, n) cin >> w[i];
    L(i, 1, n - 1)
    {
    	int u, v;
    	cin >> u >> v;
    	add(u, v), add(v, u);
    }
    dfs1(r, 0);
    dfs2(r, r);
    build(1, n, 1);
    while(m --)
    {
    	int opt, x, y, z;
    	cin >> opt;
    	if(opt == 1)  cin >> x >> y >> z, path_update(x, y, z);
    	else if(opt == 2) cin >> x >> y, cout << path_query(x, y) << '\n';
    	else if(opt == 3) cin >> x >> z, son_update(x, z);
    	else cin >> x, cout << son_query(x) << '\n';
    }
    return 0;
}

到这里就能很轻松的解决树上修改等一系列问题了,接下来给出几道例题。

1.P3038 [USACO11DEC]Grass Planting G(思考如何边权转点权)

2. P4427 [BJOI2018]求和(预处理+树剖)

3. P1505 [国家集训队]旅游(很裸的树剖,不懂为什么评紫,注意节点编号是从0开始的即可)

posted @   Svemit  阅读(118)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示
主题色彩