重链剖分
前置知识:
- 线段树, d f s dfs dfs序,基本的树上知识,效率高的存图方法(如链式前向星、邻接表)
1. 概念(chě dàn):
一种对树进行划分的算法,它先通过重链剖分将树分为多条链,
保证每个点属于且只属于一条链,然后再通过数据结构(树状数组、线段树等)来维护每一条链。
一般用于解决树上的求 ∑ \sum ∑ 和 求 max / min \max/\min max/min 等问题。
2.定义(源于OI-Wiki):
重子节点(重儿子): 表示其子节点中子树最大的子结点。如果有多个子树最大的子结点,取其一。如果没有子节点,就无重子节点。
轻子节点(轻儿子): 除重子节点外的所有节点。
重边: 从某个结点到重子节点的边。
轻边: 从某个结点到轻子节点的边。
重链: 若干条首尾衔接的重边构成的边。
Tips: 落单的结点也当作重链。
如图:
∙ \bullet ∙ f a [ x ] : fa[x]: fa[x]:节点 x x x在树上的父亲。
∙ \bullet ∙ d e p [ x ] : dep[x]: dep[x]:节点 x x x在树上的深度。
∙ \bullet ∙ s i z [ x ] : siz[x]: siz[x]:节点 x x x的子树的数量。
∙ \bullet ∙ s o n [ x ] : son[x]: son[x]:节点 x x x的重儿子。
∙ \bullet ∙ t o p [ x ] : top[x]: top[x]:节点 x x x所在重链的顶部节点。
∙ \bullet ∙ i d [ x ] : id[x]: id[x]:节点 x x x的 d f s dfs dfs序。
然后会用两次 d f s dfs dfs求解, d f s 1 dfs1 dfs1求解前三个, d f s 2 dfs2 dfs2求解后三个。
void dfs1(int x, int f, int deep) {
//x当前节点,f父亲,deep深度
dep[x] = deep, fa[x] = f, siz[x] = 1; //深度,父亲,子树大小
int maxson = -1; //求重儿子
for (int i = head[x]; i; i = E[i].next) {
//遍历
int v = E[i].to;
if (v == f) continue; //如果下一条边是父亲,那么就不遍历了,我们要找的是儿子
dfs1(v, x, deep + 1); //遍历儿子
siz[x] = siz[x] + siz[v];
if (siz[v] > maxson) //找重儿子
son[x] = v, maxson = siz[v];
}
}
void dfs2(int x, int Top) {
//x当前节点,Top是x所在重链顶部的节点
id[x] = ++cnt, wt[cnt] = w[x], top[x] = Top; //dfs序,重链顶部的点
if (!son[x]) return; //优先找重儿子
dfs2(son[x], Top); //遍历重儿子
for (int i = head[x]; i; i = E[i].next) {
//遍历
int v = E[i].to;
if (v == fa[x] || v == son[x]) continue; //找完重儿子,再去找轻儿子
dfs2(v, v);
}
}
3.性质(重链)
1. 1. 1.树上的每个节点都属于且仅属于一条重链。
2. 2. 2.所有的重链把树完全剖分。
3. 3. 3.在剖分时,优先遍历重边、重儿子。
4. 4. 4.一条链上的 d f s dfs dfs序是连续的。
5. 5. 5.每一个子树上的 d f s dfs dfs序是连续的。
4.常见应用
① ① ①路径上的维护
- 用树链的性质来求树上的边权或点权。
根据第 4 4 4条性质可知,在链上的 d f s dfs dfs序时连续的,所以我们可以用线段树进行修改、维护,每次选择深度较大的链往上跳,在跳的同时统计权值。int Query_Range(int x, int y) { int ans = 0; while (top[x] ^ top[y]) { //如果不在同一条链上 if (dep[top[x]] < dep[top[y]]) swap(x, y); //从深度大的往深度小的跳 ans = ans + Query(1, id[top[x]], id[x]); //统计链上的权值 x = fa[top[x]]; //往高处跳 } if (dep[x] > dep[y]) swap(x, y); //线段树需要满足 L <= R ans = (ans + Query(1, id[x], id[y])); //跳到最后肯定会在同一条链上 return ans % P; }
② ② ②子树维护
- 比如说求以 x x x为根节点的子树上的边权或点权。
根据第 5 5 5条性质可知,在子树上的 d f s dfs dfs序也是连续的,所以直接用线段树修改、维护就行了。int Query_Son(int x) { return Query(1, id[x], id[x] + siz[x] - 1);//子树大小的计算包括自己,所以-1 }
③ ③ ③求LCA
- 求两个点的LCA
int lca(int u, int v) { while (top[u] ^ top[v]) { //不在同一条链上 if (dep[top[u]] > dep[top[v]]) //往高处跳 u = fa[top[u]]; else v = fa[top[v]]; } return dep[u] > dep[v] ? v : u; //往高处跳 }
5.例题
P2590 [ZJOI2008]树的统计(模板)
题目描述:
一棵树上有 n n n 个节点,编号分别为 1 1 1 到 n n n,每个节点都有一个权值 w w w。
我们将以下面的形式来要求你对这棵树完成一些操作:
I . I. I. CHANGE u t : 把节点 u u u 的权值改为 t t t。
I I . II. II. QMAX u v: 询问从点 u u u 到点 v v v 的路径上的节点的最大权值。
I I I . III. III. QSUM u v: 询问从点 u u u 到点 v v v 的路径上的节点的权值和。
注意:从点 u u u 到点 v v v 的路径上的节点包括 u u u 和 v v v 本身。
输入格式:
输入文件的第一行为一个整数 n n n,表示节点的个数。
接下来 n − 1 n-1 n−1 行,每行 2 2 2 个整数 a a a 和