目录
重链剖分
概念
树链剖分可将树划分成若干条不相交的链的形式,每条链都是自底向上的。
重链剖分能保证划分出的每条重链与轻链上的节点 DFS 序连续,并以此作为节点的序号,每颗子树中的所有节点序号在
[
a
,
a
+
s
i
z
e
]
[a,a+size]
[a,a+size]范围内(子树根节点序号为
a
a
a,子树大小为
s
i
z
e
size
size)。
因此可以方便地用一些维护序列的数据结构(如线段树)来维护树上路径的信息。
下列列举几个应用:
- 修改、查询树上两节点之间的路径上所有节点的权值。
- 修改、查询以某一节点为根的子树上所有节点的权值。
- 查询两节点的最近公共祖先(lca)。
名词 | 解释 |
---|---|
重儿子 | 对于每个非叶子节点,子树大小最大的儿子 |
轻儿子 | 对于每个非叶子节点,除重儿子以外的儿子 |
重边 | 从该节点到重儿子的边 |
轻边 | 除重边以外的边 |
重链 | 由若干条首尾衔接的重边构成 |
轻链 | 由若干条首尾衔接的轻边构成 |
特别地:对于只有一个儿子的节点,该儿子即为重儿子。
模板题链接(洛谷 P3384 【模板】轻重链剖分)
预处理 O ( 2 n ) O(2n) O(2n)
①DFS1()
处理每个节点的父亲、重儿子、子树大小、深度
变量 | 解释 |
---|---|
fa[ ] | 父亲 |
son[ ] | 重儿子 |
sz[ ] | 子树大小 |
dep[ ] | 深度 |
void dfs1(int fath, int u)
{
fa[u] = fath;
dep[u] = dep[fath] + 1;
sz[u] = 1;
for (int i = h[u]; i; i = ne[i])
{
int v = e[i];
if (v == fath)continue;
dfs1(u, v);
sz[u] += sz[v];
if (sz[v] > sz[son[u]])son[u] = v;
}
}
数字为DFS序,红色节点为重儿子
②DFS2()
根据重儿子DFS重链,使其DFS序连续,并标记为新编号,初始化权值
变量 | 解释 |
---|---|
top[ ] | 当前节点所在链的最顶端节点 |
id[ ] | 节点的新编号(即为DFS序) |
w[ ] | 新编号对应的点权值 |
void dfs2(int u, int ptop)
{
id[u] = ++cnt;
w[cnt] = wt[u];
top[u] = ptop;
if (!son[u])return;
dfs2(son[u], ptop);//先dfs重链,保证重链编号连续
for (int i = h[u]; i; i = ne[i])
{
int v = e[i];
if (v != fa[u] && v != son[u])
dfs2(v, v);//轻链的顶端节点就是自己
}
}
数字为DFS序(即新编号),紫色边为重链,红色节点为重儿子,不难发现子树的编号也是连续的
修改查询操作
①对于任意两节点间的路径上所有节点
修改 O ( l o g 2 n ) O(log^{2}n) O(log2n)
如果两个节点处于两条不同的重链,则应让深度更深的节点跳到该链顶端的父亲节点(类似LCA),并在此期间要使用数据结构维护路径上的权值(同一条链上的编号连续)。
void modify(int a, int b, int z)//给a,b两节点间的路径权值加上z
{
while (top[a] != top[b])
{
if (dep[top[a]] < dep[top[b]])swap(a, b);//注意要让该链顶端节点更深的的节点先跳
//(易错)不是 dep[a] < dep[b]
update(1, 1, n, id[top[a]], id[a], z);
a = fa[top[a]];
}
if (dep[a] > dep[b])swap(a, b);
update(1, 1, n, id[a], id[b], z);
}
查询 O ( l o g 2 n ) O(log^{2}n) O(log2n)
思路与修改类似
ll ask(int a, int b)
{
ll ans = 0;
while (top[a] != top[b])
{
if (dep[top[a]] < dep[top[b]])swap(a, b);
ans += query(1, 1, n, id[top[a]], id[a]);
a = fa[top[a]];
}
if (dep[a] > dep[b])swap(a, b);
ans += query(1, 1, n, id[a], id[b]);
return ans;
}
②对于以任意节点为根的子树
由于子树的编号是连续的,前面的预处理中已标记每个节点的子树大小,因此可以直接使用数据结构维护
修改 O ( l o g n ) O(logn) O(logn)
void modify(int x, int z)//给以z为根的子树加上z
{
update(1, 1, n, id[x], id[x] + sz[x] - 1, z);
}
查询 O ( l o g n ) O(logn) O(logn)
ll ask(int x)
{
return query(1, 1, n, id[x], id[x] + sz[x] - 1);
}
数据结构
数据结构可以灵活选择,这里不作赘述,完整代码中的数据结构为线段树
完整代码(无注释)
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 2e5 + 10;
ll wt[N], w[N], P;
int n, m, r;
#define lp p<<1
#define rp p<<1|1
#define lson lp,l,mid
#define rson rp,mid+1,r
struct { ll sum, tag, len; }t[N << 2];
void build(int p, int l, int r)
{
t[p].len = r - l + 1;
if (l == r)
{
t[p].sum = w[l] % P;
return;
}
int mid = l + r >> 1;
build(lson), build(rson);
t[p].sum = (t[lp].sum + t[rp].sum) % P;
}
void pushdown(int p)
{
if (!t[p].tag)return;
t[lp].sum = (t[lp].sum + t[p].tag * t[lp].len) % P;
t[rp].sum = (t[rp].sum + t[p].tag * t[rp].len) % P;
t[lp].tag = (t[lp].tag + t[p].tag) % P;
t[rp].tag = (t[rp].tag + t[p].tag) % P;
t[p].tag = 0;
}
void update(int p, int l, int r, int L, int R, int z)
{
if (l >= L && R >= r)
{
t[p].sum = (t[p].sum + t[p].len * z) % P;
t[p].tag = (t[p].tag + z) % P;
return;
}
pushdown(p);
int mid = l + r >> 1;
if (L <= mid)update(lson, L, R, z);
if (R > mid)update(rson, L, R, z);
t[p].sum = (t[lp].sum + t[rp].sum) % P;
}
ll query(int p, int l, int r, int L, int R)
{
if (l >= L && R >= r)return t[p].sum;
pushdown(p);
int mid = l + r >> 1;
ll ans = 0;
if (L <= mid)ans = (ans + query(lson, L, R)) % P;
if (R > mid)ans = (ans + query(rson, L, R)) % P;
return ans;
}
//==================================================
int e[N], ne[N], h[N], idx;
int fa[N], dep[N], id[N], son[N], top[N], sz[N], cnt;
void add(int u, int v)
{
e[++idx] = v;
ne[idx] = h[u];
h[u] = idx;
}
void dfs1(int fath, int u)
{
fa[u] = fath;
dep[u] = dep[fath] + 1;
sz[u] = 1;
for (int i = h[u]; i; i = ne[i])
{
int v = e[i];
if (v == fath)continue;
dfs1(u, v);
sz[u] += sz[v];
if (sz[v] > sz[son[u]])son[u] = v;
}
}
void dfs2(int u, int ptop)
{
id[u] = ++cnt;
w[cnt] = wt[u];
top[u] = ptop;
if (!son[u])return;
dfs2(son[u], ptop);
for (int i = h[u]; i; i = ne[i])
{
int v = e[i];
if (v != fa[u] && v != son[u])
dfs2(v, v);
}
}
//================================================
void op_1(int a, int b, int z)
{
while (top[a] != top[b])
{
if (dep[top[a]] < dep[top[b]])swap(a, b);
update(1, 1, n, id[top[a]], id[a], z);
a = fa[top[a]];
}
if (dep[a] > dep[b])swap(a, b);
update(1, 1, n, id[a], id[b], z);
}
ll op_2(int a, int b)
{
ll ans = 0;
while (top[a] != top[b])
{
if (dep[top[a]] < dep[top[b]])swap(a, b);
ans = (ans + query(1, 1, n, id[top[a]], id[a])) % P;
a = fa[top[a]];
}
if (dep[a] > dep[b])swap(a, b);
ans = (ans + query(1, 1, n, id[a], id[b])) % P;
return ans;
}
void op_3(int x, int z)
{
update(1, 1, n, id[x], id[x] + sz[x] - 1, z);
}
ll op_4(int x)
{
return query(1, 1, n, id[x], id[x] + sz[x] - 1);
}
int main()
{
scanf("%d%d%d%lld", &n, &m, &r, &P);
for (int i = 1; i <= n; i++)
scanf("%d", &wt[i]);
for (int i = 1; i < n; i++)
{
int u, v;
scanf("%d%d", &u, &v);
add(u, v), add(v, u);
}
dfs1(0, r);
dfs2(r, r);
build(1, 1, n);
while (m--)
{
int op, x, y, z;
scanf("%d", &op);
switch (op)
{
case 1: scanf("%d%d%d", &x, &y, &z), op_1(x, y, z); break;
case 2: scanf("%d%d", &x, &y), printf("%lld\n", op_2(x, y)); break;
case 3: scanf("%d%d", &x, &z), op_3(x, z); break;
case 4: scanf("%d", &x), printf("%lld\n", op_4(x)); break;
}
}
return 0;
}
拓展
(21.5.12更新)
若查询修改的是边权而不是点权,步骤基本上差不多,只需要在dfs1()中将边权赋予儿子。
拿上面的树砍掉一半作为示例,4条边的权值分别为
w
1
w_1
w1、
w
2
w_2
w2、
w
3
w_3
w3、
w
4
w_4
w4,求节点8到节点7路上的权值之和,易知答案为
w
2
+
w
3
+
w
4
w_2+w_3+w_4
w2+w3+w4。
将边权赋予儿子之后,在查询时注意不能直接搬查询点权的模板。
在查询点权的模板中,当两节点跳到同一条重链时会计算
w
1
+
w
2
w_1+w_2
w1+w2,而实际上只能计算
w
2
w_2
w2。
当两节点跳到同一条重链时,设这两个点分别为
u
、
v
u、v
u、v,则深度较浅的必定为最开始时两节点的LCA,令这一节点为
u
u
u,我们可以显然可以知道只能计算
[
i
d
[
u
]
+
1
,
i
d
[
v
]
]
[id[u]+1,id[v]]
[id[u]+1,id[v]]间的权值。(当
u
=
v
u=v
u=v时不作处理)
参考文章:
https://www.cnblogs.com/chinhhh/p/7965433.html
https://oi-wiki.org/graph/hld/
https://www.cnblogs.com/ivanovcraft/p/9019090.html