【笔记/模板】树链剖分

树链剖分

树链剖分的基本思想

通过将树分割成链的形式,从而把树形变为线性结构,减少处理难度。

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

树链有如下几个特征:

  1. 一棵树上的任意一条链的长度不超过 \(\log_2 n\),且一条链上的各个节点深度互不相同(相对于根节点而言)。
  2. 通过特殊的遍历方式,树链剖分可以保证一条链上的 DFS 序连续,从而更加方便地使用线段树或树状数组维护树上的区间信息。

重链剖分

重链剖分的基本定义

重链剖分,顾名思义,是一种通过子节点大小进行剖分的形式,我们给出以下定义:

  1. 重子节点:一个非叶子节点中子树最大的子节点,如果存在多个则取其一。

  2. 轻子节点:除了重子节点以外的所有子节点。

  3. 重边:连接任意两个重儿子的边。

  4. 轻边:除了重边的其他所有树边。

  5. 重链:若干条重边首尾相连而成的一条链。

我们将落单的叶子节点本身看做重子节点,不难发现,整棵树就被剖分成了一条条重链。

需要注意的是,每一条重链都以轻子节点为起点。

实现

树链剖分的处理通过两次 DFS 遍历完成。

对于第一次 DFS,我们求出如下数值:

  • 任意节点到达根的距离(即其深度):depth[]

  • 任意节点的父亲节点(根节点默认为 \(0\)):fa[]

  • 任意节点子树的大小(包括其本身):sz[]

  • 任意节点的重子节点(没有则为 \(0\)):hson[]

void dfs1(int ver, int pre, int deep)
{
	depth[ver] = deep, fa[ver] = pre, sz[ver] = 1;
	int maxn = -1;
	for (int i = h[ver]; ~i; i = ne[i])
	{
		int j = e[i];
		if (j == pre) continue;
		dfs1(j, ver, deep + 1);
		sz[ver] += sz[j];
		if (maxn == -1 || maxn < sz[j]) maxn = sz[j], hson[ver] = j;
        	// 更新重子节点
	}
}

对于第二次 DFS,我们求出如下数值:

  • 遍历时的各个节点的 dfs 序:dfn[]

  • 每个节点所属重链的最顶端节点:top[]

  • dfs 序对应的节点编号:id[],有 \(id(dfn(x)) = x\)

  • 每个节点在 dfs 序上的对应权值:val[]

void dfs2(int ver, int topf)
{
	dfn[ver] = ++ timestamp, val[timestamp] = a[ver], top[ver] = topf;
	if (!hson[ver]) return;
	dfs2(hson[ver], topf);	// 先遍历重子节点
	
	for (int i = h[ver]; ~i; i = ne[i])
	{
		int j = e[i];
		if (j == fa[ver] || j == hson[ver]) continue;
		dfs2(j, j);	// 再遍历轻子节点
	}
}

之所以要先遍历重子节点,是因为我们要保证重链上的 dfs 序连续,这样才可以进行区间操作,按照 \(dfn\) 排序后的序列即为剖分后的链。

重链剖分的性质

  1. 树上每个节点都属于且仅属于一条重链

  2. 所有的重链将整棵树 完全剖分

  3. 当我们向下经过一条 轻边 时,所在子树的大小至少会除以二,保证了复杂度的正确性。

常见应用

维护路径权值和

选取左右端点所在树中深度更大的节点,维护它到所在重链顶端的区间信息,之后不断上跳,知道它和另一端点在同一链上,维护两点之间的信息。使用线段树或者树状数组等数据结构,即可在 \(O(\log^2 n)\)​ 的时间内单次维护查询。

void modify_range(int x, int y, int k)
{
	while (top[x] != top[y])
	{
		if (depth[top[x]] < depth[top[y]]) swap(x, y);
		SGT.modify(1, dfn[top[x]], dfn[x], k);
		x = fa[top[x]];
	}
	
	if (depth[x] > depth[y]) swap(x, y);
	SGT.modify(1, dfn[x], dfn[y], k);
}
int query_range(int x, int y)
{
	int res = 0;
	while (top[x] != top[y])
	{
		if (depth[top[x]] < depth[top[y]]) swap(x, y);
		res = (res + SGT.query(1, dfn[top[x]], dfn[x])) % mod;
		x = fa[top[x]];
	}
	
	if (depth[x] > depth[y]) swap(x, y);
	res = (res + SGT.query(1, dfn[x], dfn[y])) % mod;
	return res;
}

维护子树信息

思路相似,但更加简单,经过 dfn 重新划分后,一颗子树的 dfn 序列一定在 \([dfn[x], dfn[x] +sz[x] - 1]\) 之间,单次维护即可,时间复杂度 \(O(\log n)\)

void modify_subtree(int x, int k)
{
	SGT.modify(1, dfn[x], dfn[x] + sz[x] - 1, k);
}
int query_subtree(int x)
{
	return SGT.query(1, dfn[x], dfn[x] + sz[x] - 1);
}

求 LCA

与倍增求法相似,但常数更小。

每次选取重链顶端节点深度更大的节点上跳,知道两者在同一重链上,此时深度较小者为两节点 LCA。

int lca(int a, int b)
{
	while (top[a] != top[b])
    {
		if (depth[top[a]] > depth[top[b]]) a = fa[top[a]];
       	else b = fa[top[b]];
    }
    return depth[a] < depth[b] ? a : b;
}

例题 & Code

P3384 【模板】重链剖分/树链剖分

// Problem: P3384 【模板】重链剖分/树链剖分
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3384
// Memory Limit: 128 MB
// Time Limit: 1000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>

using namespace std;

// #define int long long
#define DEBUG
#define lc u << 1
#define rc u << 1 | 1
#define File(a) freopen(a".in", "r", stdin); freopen(a".out", "w", stdout)

typedef long long LL;
typedef pair<int, int> PII;


const int N = 100010, M = N << 1;
const int INF = 0x3f3f3f3f;

int n, m, root, mod;
int h[N], e[M], ne[M], idx;
int a[N], val[N];
int depth[N], fa[N], sz[N], hson[N];
int dfn[N], timestamp;
int top[N], id[N];

struct Tree
{
	struct Node
	{
		int l, r, sum, tag;
		inline int len() {return r - l + 1; }
	} tr[N << 2];
	
	void pushup(int u)
	{
		tr[u].sum = (tr[lc].sum + tr[rc].sum) % mod;
	}
	
	void build(int u, int l, int r)
	{
		tr[u].l = l, tr[u].r = r;
		if (l == r) return tr[u].sum = val[l], void(0);
		int mid = l + r >> 1;
		build(lc, l, mid), build(rc, mid + 1, r);
		pushup(u);
	}
	
	void pushdown(int u)
	{
		if (!tr[u].tag) return;
		tr[lc].sum = (tr[lc].sum + tr[u].tag * tr[lc].len()) % mod;
		tr[rc].sum = (tr[rc].sum + tr[u].tag * tr[rc].len()) % mod;
		tr[lc].tag += tr[u].tag, tr[rc].tag += tr[u].tag;
		tr[u].tag = 0;
	}
	
	void modify(int u, int l, int r, int k)
	{
		if (l <= tr[u].l && tr[u].r <= r)
		{
			tr[u].sum = (tr[u].sum + tr[u].len() * k) % mod;
			tr[u].tag += k;
			return;
		}
		
		pushdown(u);
		int mid = tr[u].l + tr[u].r >> 1;
		if (l <= mid) modify(lc, l, r, k);
		if (r > mid) modify(rc, l, r, k);
		pushup(u);
	}
	
	int query(int u, int l, int r)
	{
		if (l <= tr[u].l && tr[u].r <= r)
			return tr[u].sum;
		pushdown(u);
		int mid = tr[u].l + tr[u].r >> 1;
		int res = 0;
		if (l <= mid) res = (res + query(lc, l, r)) % mod;
		if (r > mid) res = (res + query(rc, l, r)) % mod;
		return res;
	}
} SGT;

inline void add(int a, int b)
{
	e[++ idx] = b, ne[idx] = h[a], h[a] = idx;
}

void dfs1(int ver, int pre, int deep)
{
	depth[ver] = deep, fa[ver] = pre, sz[ver] = 1;
	int maxn = -1;
	for (int i = h[ver]; ~i; i = ne[i])
	{
		int j = e[i];
		if (j == pre) continue;
		dfs1(j, ver, deep + 1);
		sz[ver] += sz[j];
		
		if (maxn == -1 || maxn < sz[j]) maxn = sz[j], hson[ver] = j;
	}
}

void dfs2(int ver, int topf)
{
	dfn[ver] = ++ timestamp, val[timestamp] = a[ver], top[ver] = topf;
	if (!hson[ver]) return;
	dfs2(hson[ver], topf);
	
	for (int i = h[ver]; ~i; i = ne[i])
	{
		int j = e[i];
		if (j == fa[ver] || j == hson[ver]) continue;
		dfs2(j, j);
	}
}

void modify_range(int x, int y, int k)
{
	while (top[x] != top[y])
	{
		if (depth[top[x]] < depth[top[y]]) swap(x, y);
		SGT.modify(1, dfn[top[x]], dfn[x], k);
		x = fa[top[x]];
	}
	
	if (depth[x] > depth[y]) swap(x, y);
	SGT.modify(1, dfn[x], dfn[y], k);
}

int query_range(int x, int y)
{
	int res = 0;
	while (top[x] != top[y])
	{
		if (depth[top[x]] < depth[top[y]]) swap(x, y);
		res = (res + SGT.query(1, dfn[top[x]], dfn[x])) % mod;
		x = fa[top[x]];
	}
	
	if (depth[x] > depth[y]) swap(x, y);
	res = (res + SGT.query(1, dfn[x], dfn[y])) % mod;
	return res;
}

void modify_subtree(int x, int k)
{
	SGT.modify(1, dfn[x], dfn[x] + sz[x] - 1, k);
}

int query_subtree(int x)
{
	return SGT.query(1, dfn[x], dfn[x] + sz[x] - 1);
}

signed main()
{
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	memset(h, -1, sizeof h);
	cin >> n >> m >> root >> mod;
	for (int i = 1; i <= n; i ++) cin >> a[i];
	for (int i = 1; i < n; i ++)
	{
		int u, v; cin >> u >> v;
		add(u, v), add(v, u);
	}
	
	dfs1(root, 0, 1);
	dfs2(root, root);
	SGT.build(1, 1, n);
	
	while (m --)
	{
		int opt; cin >> opt;
		if (opt == 1)
		{
			int x, y, z; cin >> x >> y >> z;
			modify_range(x, y, z);
		}
		else if (opt == 2)
		{
			int x, y; cin >> x >> y;
			cout << query_range(x, y) % mod << '\n';
		}
		else if (opt == 3)
		{
			int x, y; cin >> x >> y;
			modify_subtree(x, y);
		}
		else
		{
			int x; cin >> x;
			cout << query_subtree(x) % mod << '\n';
		}
	}
	
	return 0;
}

Reference

树链剖分 - OI Wiki

P3384 【模板】重链剖分/树链剖分 题解

posted @   ThySecret  阅读(153)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示