【算法】树链剖分

首先声明:“剖” 读 “pou” !!!!!解剖的剖(pou)!!!!!
真不知道为什么他们都读 “pao”……

进入正题:

介绍:

树路径信息维护算法。

将一棵树划分成若干条链,用数据结构去维护每条链,复杂度为O(logN)。

其实本质是一些数据结构/算法在树上的推广

                                         —————— 360百科

大概做法:

常见的路径剖分的方法是轻重树链剖分(启发式剖分)

将树中的边分为:轻边和重边 ž定义size(X)为以X为根的子树的节点个数。 ž令V为U的儿子节点中size值最大的节点,那么边(U,V)被称为重边,树中重边之外的边被称为轻边。

性质:ž轻边(U,V),size(V)<=size(U)/2。 ž从根到某一点的路径上,不超过O(logN)条轻边,不超过O(logN)条重路径。

                                         ——————360百科

感谢360百科的友情支持(鼓掌)

前置知识:

每个节点都有一个重儿子,定义为:每个节点的最大子树的根

轻儿子定义:除了重儿子其余都是轻儿子

重边定义:重儿子与它爸爸连起来的边

轻边定义:除了重边全是轻边

重链定义:由重边组成的链

轻链定义:由轻边组成的链

给个图:

我天这什么玩意儿 号是我随便编的

毫无疑问,2 是 1 的重儿子,4 是 2 的重儿子,9 是 3 的重儿子,

而 6 , 7 , 8 都可以是 4 的重儿子(重儿子只能有一个,所以只要在 6,7,8 中随便选一个就可以了)。

划分完大概是这个亚子:

其中紫色的边为重边,绿色的点为重儿子。

具体做法 && 算法分析:

分为三个大块:

  1. 大法师一(dfs1):用于求每个点的父亲节点,重儿子,该点深度, 该(子)树大小 。

  2. 大法师二(dfs2):用于求每个点的 dfs 序,dfs 序对应着哪个点,该点所在链的顶端 。

  3. 宿命(sum):可以用于树的两点之间求和。

  4. 嫦娥(change):可以用于区间修改等等等等。

1. 大法师一(dfs1):

声明一下:

ft[i] 为 i 的父亲;
son[i] 为 i 的重儿子;
dep[i] 为 i 的深度;
size[i] 为 以 i 为根的子树的大小。

用代码说:

void dfs1(int now, int fa, int de) { // now 为现在所在的点,fa 为 now 的父亲,de 为该点深度。
	int v;
	ft[now] = fa; // 记录父亲
	dep[now] = de; // 记录深度
	size[now] = 1; // 每个点初始大小为 1 (因为包括它自己)
	for (int i = head[now]; i; i = tree[i].nxt) {
		v = tree[i].v;
		if (v != fa) { // 避免回溯回去
			dfs1(v, now, de + 1); // 搜索
			size[now] += size[v]; // 加上子树的大小
			if (size[v] > size[son[now]]) son[now] = v; // 记录重儿子
		}
	}
}

2. 大法师二(dfs2):

声明一下:

dfn[i] 为 i 的 dfs 序;
fdn[i] 为 dfs 序为 i 代表的节点;(不要在意我起名的方式)
top[i] 为 i 点所在链的顶端;

用代码说:

void dfs2(int now, int t) { // now 为当前节点,t 为当前节点所在链的顶端。
	int v;
	top[now] = t; // 记录顶端
	dfn[now] = ++cnt; // 记录 dfs 序
	fdn[cnt] = now; // 记录 dfs 序所代表的节点
	if (son[now]) dfs2(son[now], t); // 优先遍历重儿子,使其组成一条链
	for (int i = head[now]; i; i = tree[i].nxt) {
		v = tree[i].v;
		if (v != ft[now] && v != son[now]) dfs2(v, v); // 然后遍历轻儿子,轻儿子的顶端为自己
	}
}

3. 宿命(sum):

以求和为例:

首先我们知道,

轻重链剖分其实是在数据结构的基础上实现的,只不过加上了一些求 LCA 的元素而已,

那么,是哪一个数据结构呢?

让我们掌声有请:

线段树!!!(呕)

没错,就是la个一调调一天的 神(po)奇(lan)数(wan)据(yi)!!

无法否认的是,线段树确实是区间求和的很好的一种方式,考试骗分就靠它!(除了有点头疼)

代码来啦~~:

int sum(int x, int y) {
	int ans = 0;
	while (top[x] != top[y]) { // 如果两点不在同一条链上
		if (dep[top[x]] < dep[top[y]]) swap(x, y); // 保证 x 点深度比 y 大,让 x 点往上跳。
		L = dfn[top[x]], R = dfn[x];
		a_l(1);
		ans = (ans + ans_te) % p; // 加上这条链的区间值
		ans_te = 0; // 记得清零(或许有些人线段树写法跟我不一样)
		x = ft[top[x]]; // 从这个链顶跳到它的父亲
	}
	if (dep[x] > dep[y]) swap(x, y); // 保持从 x 向上跳
	L = dfn[x], R = dfn[y];
	a_l(1);
	ans = (ans + ans_te) % p;
	ans_te = 0;
	return ans % p; // 被 %p 支配的痛苦
}

4. 嫦娥(change):

上代码:

void change(int x, int y, int z) { // 大概跟 sum 差不多,改一改就行了
	C = z;
	while (top[x] != top[y]) {
		if (dep[top[x]] < dep[top[y]]) swap(x, y);
		L = dfn[top[x]]; R = dfn[x];
		c_l(1);
		x = ft[top[x]];
	}
	if (dep[x] > dep[y]) swap(x, y);
	L = dfn[x], R = dfn[y];
	c_l(1);
}

完整代码:(不多不多,只有 137 173 行)

#include <iostream>
#include <cstdio>
#include <queue>
#define int long long
#define MAXN 100010
#define LL long long
#define M(a, b) std :: make_pair(a, b)
#define F1(i, a, b) for (int i = a; i <= b; ++i)
#define F2(i, a, b) for (int i = a; i >= b; --i)
using namespace std;

int ft[MAXN], son[MAXN], dep[MAXN], size[MAXN];
int dfn[MAXN], fdn[MAXN], top[MAXN];
int head[MAXN], node[MAXN];
int cnt = 0, js = 0, n, m, r, p;
int ans_te = 0, L, R, C;

struct edge {
	int l, r, book, len;
	int sum;
}e[MAXN << 2];

struct Node {
	int v, nxt;
}tree[MAXN << 1];

void add(int u, int v) {
	tree[++js].v = v;
	tree[js].nxt = head[u];
	head[u] = js;
}

void buit(int l, int r, int k) {
	e[k].l = l;
	e[k].r = r;
	e[k].len = r - l + 1;
	if (l == r) {
		e[k].sum = (node[fdn[l]]) % p;
		return;
	}
	int mid = (l + r) >> 1;
	buit(l, mid, k << 1);
	buit(mid + 1, r, (k << 1) + 1);
	e[k].sum = (e[k << 1].sum + e[(k << 1) + 1].sum) % p;
}

void down(int k) {
	e[k << 1].book = (e[k << 1].book + (e[k].book) % p) % p;
	e[(k << 1) + 1].book = (e[(k << 1) + 1].book + (e[k].book) % p) % p;
	e[k << 1].sum = (e[k << 1].sum + (e[k].book * e[k << 1].len) % p) % p;
	e[(k << 1) + 1].sum = (e[(k << 1) + 1].sum + (e[k].book * e[(k << 1) + 1].len) % p) % p;
	e[k].book = 0;
}

void a_l(int k) {
	if (e[k].l >= L && e[k].r <= R) {
		ans_te = (ans_te + e[k].sum) % p;
		return;
	}
	if (e[k].book) down(k);
	int mid = (e[k].l + e[k].r) >> 1;
	if (L <= mid) a_l(k << 1);
	if (R > mid) a_l((k << 1) + 1);
}

void c_l(int k) {
	if (e[k].l >= L && e[k].r <= R) {
		e[k].sum = (e[k].sum + (C * e[k].len) % p) % p;
		e[k].book = (e[k].book + C) % p;
		return;
	}
	if (e[k].book) down(k);
	int mid = (e[k].l + e[k].r) >> 1;
	if (L <= mid) c_l(k << 1);
	if (R > mid) c_l((k << 1) + 1);
	e[k].sum = (e[k << 1].sum + e[(k << 1) + 1].sum) % p;
}

void dfs1(int now, int fa, int de) {
	int v;
	ft[now] = fa;
	dep[now] = de;
	size[now] = 1;
	for (int i = head[now]; i; i = tree[i].nxt) {
		v = tree[i].v;
		if (v != fa) {
			dfs1(v, now, de + 1);
			size[now] += size[v];
			if (size[v] > size[son[now]]) son[now] = v;
		}
	}
}

void dfs2(int now, int t) {
	int v;
	top[now] = t;
	dfn[now] = ++cnt;
	fdn[cnt] = now;
	if (son[now]) dfs2(son[now], t);
	for (int i = head[now]; i; i = tree[i].nxt) {
		v = tree[i].v;
		if (v != ft[now] && v != son[now]) dfs2(v, v);
	}
}

int sum(int x, int y) {
	int ans = 0;
	while (top[x] != top[y]) {
		if (dep[top[x]] < dep[top[y]]) swap(x, y);
		L = dfn[top[x]], R = dfn[x];
		a_l(1);
		ans = (ans + ans_te) % p;
		ans_te = 0;
		x = ft[top[x]];
	}
	if (dep[x] > dep[y]) swap(x, y);
	L = dfn[x], R = dfn[y];
	a_l(1);
	ans = (ans + ans_te) % p;
	ans_te = 0;
	return ans % p;
}

void change(int x, int y, int z) {
	C = z;
	while (top[x] != top[y]) {
		if (dep[top[x]] < dep[top[y]]) swap(x, y);
		L = dfn[top[x]]; R = dfn[x];
		c_l(1);
		x = ft[top[x]];
	}
	if (dep[x] > dep[y]) swap(x, y);
	L = dfn[x], R = dfn[y];
	c_l(1);
}

signed main() {
	int x, y, z, c;
	scanf("%lld %lld %lld %lld", &n, &m, &r, &p);
	F1(i, 1, n) scanf("%lld", &node[i]);
	F1(i, 1, n - 1) {
		scanf("%lld %lld", &x, &y);
		add(x, y); add(y, x);
	}
	dfs1(r, 0, 1);
	dfs2(r, r);
	buit(1, n, 1);
	F1(i, 1, m) {
		ans_te = 0;
		scanf("%lld", &c);
		if (c == 1) {
			scanf("%lld %lld %lld", &x, &y, &z);
			change(x, y, z);
		}
		else if (c == 2) {
			scanf("%lld %lld", &x, &y);
			printf("%lld\n", sum(x, y));
		}
		else if (c == 3) {
			scanf("%lld %lld", &x, &z);
			C = z; L = dfn[x]; R = dfn[x] + size[x] - 1;
			c_l(1);
		}
		else if (c == 4) {
			ans_te = 0;
			scanf("%lld", &x);
			L = dfn[x]; R = dfn[x] + size[x] - 1;
			a_l(1);
			printf("%lld\n", ans_te);
		}
	}
	return 0;
}

给两个模板题:

  1. 洛谷 P3384 【模板】轻重链剖分

  2. 洛谷 GSS7 - Can you answer these queries VII

(坏笑)

posted @ 2020-10-25 11:33  _Scaley  阅读(250)  评论(2编辑  收藏  举报