最近在机房自习复习了一下这些东西,主要给自己看的。
参考文献:
-
《算法竞赛》(罗勇军,郭卫斌 著)
1 重链剖分简介
1.1 重链剖分基本性质
树链剖分,就是把树分成若干条链,然后通过这些链来进行各种操作。同时树链剖分分为重链剖分和长链剖分,一般情况下,我们所说的树链剖分就是重链剖分。
重链的定义:
-
一个节点子树大小最大的点被称为这个节点的重子节点(又名重儿子,可能不存在),这个点到它重子节点的边被称之为重边。
-
若干个首尾相连的重边就是重链。
-
不是重子节点的点就是轻子节点,不是重边就是轻边。
重链的性质:
-
是一条链,且链中所有点的 LCA 是深度最浅的那个端点。也就是链上的点深度互不相同。
-
一棵树的链不超过 \(O(\log\ n)\) 条。
-
每一个点都在且仅在一条重链当中(也就是说,单独一个点也可以是一条重链)。
想要写出完整的重链剖分,你需要维护以下东西:
-
\(siz(x),son(x),fa(x),dep(x)\),分别表示当前节点的子树大小,重儿子是谁,以及父亲节点是谁,其深度。
-
\(top(x)\) 其所在重链为的深度最浅的节点。
-
\(dfn(x),rnk(x)\),分别表示 \(x\) 号点在重子节点优先遍历中的搜索序和搜索序为 \(x\) 的节点是谁。
例如以下就是一个合法重链剖分方案:
1.2 实现重链剖分
int n, son[N], dfn[N], rnk[N], top[N], siz[N], dep[N], fa[N], idx;
void dfs1(int x, int f) { // 维护 siz(x),dep(x),fa(x),son(x)。
siz[x] = 1, dep[x] = dep[f] + 1, fa[x] = f;
son[x] = -1;
for (int i : v[x]) {
if (i == f) {
continue;
}
dfs1(i, x);
siz[x] += siz[i];
if (son[x] == -1 || siz[i] > siz[son[x]]) {
son[x] = i;
}
}
}
void dfs2(int x, int tp) { // 实现重链剖分。
top[x] = tp;
dfn[x] = ++idx, rnk[idx] = x;
if (son[x] == -1) {
return;
}
dfs2(son[x], tp); // 优先便利重子节点,这样才能保证重链的连续性。
for (int i : v[x]) {
if (i == fa[x] || i == son[x]) {
continue;
}
dfs2(i, i);
}
}
2 重链剖分的常见应用
2.1 最近公共祖先
对于两个点,不断跳到这一条重链之上,当两点位于同一条重链时,深度小者为两点最近公共祖先。
时间复杂度 \(O(\log\ n)\),常数较优。
int lca(int x, int y) {
while (top[x] != top[y]) {
if (dep[top[x]] > dep[top[y]]) {
x = fa[top[x]];
} else {
y = fa[top[y]];
}
}
return dep[x] < dep[y] ? x : y;
}
2.2 维护两点间路径权值和(查询+修改)
首先,我们都知道一条重链中,他们的 \(dfn\) 是连续的,因此我们可以用一些数据结构来维护(包括但不限于线段树、树状数组…)。对于,我们每次将深度大的点往上跳,并加上当前链的权值和。对于修改也是大同小异,具体看代码。
时间复杂度为 \(O((\log\ n)^2)\),常数较优。
// seg 是线段树所在结构体的部分,读者请自行实现支持区间查询、区间修改的数据结构。
void modify_add(int x, int y, int z) { // 修改操作。
while (top[x] != top[y]) {
if (dep[top[x]] > dep[top[y]]) {
seg.modify(1, z, dfn[top[x]], dfn[x]); // 将 [dfn[top[x]], top[x]] 加上 z。
x = fa[top[x]];
} else {
seg.modify(1, z, dfn[top[y]], dfn[y]);
y = fa[top[y]];
}
}
if (dep[x] < dep[y]) {
seg.modify(1, z, dfn[x], dfn[y]);
} else {
seg.modify(1, z, dfn[y], dfn[x]);
}
}
int query_sum(int x, int y) { // 查询部分。
int res = 0;
while (top[x] != top[y]) {
if (dep[top[x]] > dep[top[y]]) {
(res += seg.query(1, dfn[top[x]], dfn[x])) %= p; // 查询 [dfn[top[x]], dfn[x]] 的权值和。
x = fa[top[x]];
} else {
(res += seg.query(1, dfn[top[y]], dfn[y])) %= p;
y = fa[top[y]];
}
}
if (dep[x] < dep[y]) {
(res += seg.query(1, dfn[x], dfn[y])) %= p;
} else {
(res += seg.query(1, dfn[y], dfn[x])) %= p;
}
return res;
}
其实按照同样的方法可以实现求极值,读者可以尝试自行实现。(参考 洛谷 P2590 [ZJOI2008]树的统计)
2.3 维护某点子树权值(查询+修改)
不难发现,一个以 \(x\) 为根的子树内的 \(dfn\) 也是连续的,并且其值是 \(dfn(x)\) 到 \(dfn(x)+siz(x)-1\)。
时间复杂度 \(O(\log\ n)\)。
seg.modify(1, z, dfn[x], dfn[x] + siz[x] - 1); // 修改。
seg.query(1, dfn[x], dfn[x] + siz[x] - 1) % p; // 查询。
2.4 其它的东西
讲些实践中的细节。
-
对于维护的是边权而非点权,可以先将边权下传成为点权。但是注意,操作间点的 LCA 的权值需要无视。(参考 洛谷 P3038 [USACO11DEC]Grass Planting G)
-
注意修改 \(x\) 的子树是 \(dfn(x)\)(线段树上改),修改两点间的距离就是 \(x,y\)(直接在树上改)。
3 习题
3.1 习题1
题目大意:
已知一棵包含 \(N\) 个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:
1 x y z
,表示将树从 \(x\) 到 \(y\) 结点最短路径上所有节点的值都加上 \(z\)。
2 x y
,表示求树从 \(x\) 到 \(y\) 结点最短路径上所有节点的值之和。
3 x z
,表示将以 \(x\) 为根节点的子树内所有节点值都加上 \(z\)。
4 x
表示求以 \(x\) 为根节点的子树内所有节点值之和
思路:
没什么好解释的,写个线段树再将前文所示的代码缝合一下即可。
参考代码(部分):
const int N = 1e5 + 5;
int n, m, s, p, w[N], son[N], dfn[N], rnk[N], top[N], siz[N], dep[N], fa[N], idx;
vector<int> v[N];
struct SegmentTree { // 线段树模板,不解释。
struct Tree {
int l, r, x, add;
} tr[N << 2];
void pushup(int u) {
tr[u].x = tr[u << 1].x + tr[u << 1 | 1].x;
}
void pushdown(int u) {
Tree &root = tr[u], &left = tr[u << 1], &right = tr[u << 1 | 1];
if (root.add) {
(left.add += root.add) %= p;
(left.x += (left.r - left.l + 1) * root.add) %= p;
(right.add += root.add) %= p;
(right.x += (right.r - right.l + 1) * root.add) %= p;
root.add = 0;
}
}
void build(int u, int l, int r) {
tr[u].l = l, tr[u].r = r, tr[u].add = 0;
if (l == r) {
tr[u].x = w[rnk[l]];
return;
}
int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
pushup(u);
}
int query(int u, int l, int r) {
if (tr[u].l >= l && tr[u].r <= r) return tr[u].x;
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1, res = 0;
if (mid >= l) (res += query(u << 1, l, r)) %= p;
if (mid < r) (res += query(u << 1 | 1, l, r)) %= p;
return res;
}
void modify(int u, int d, int l, int r) {
if (tr[u].l >= l && tr[u].r <= r) {
(tr[u].x += (tr[u].r - tr[u].l + 1) * d) %= p;
(tr[u].add += d) %= p;
} else {
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) modify(u << 1, d, l, r);
if (r > mid) modify(u << 1 | 1, d, l, r);
pushup(u);
}
}
} seg;
void dfs1(int x, int f) { // 树链剖分预处理部分。
siz[x] = 1, dep[x] = dep[f] + 1, fa[x] = f;
son[x] = -1;
for (int i : v[x]) {
if (i == f) continue;
dfs1(i, x);
siz[x] += siz[i];
if (son[x] == -1 || siz[i] > siz[son[x]]) son[x] = i;
}
}
void dfs2(int x, int tp) {
top[x] = tp;
dfn[x] = ++idx, rnk[idx] = x;
if (son[x] == -1) return;
dfs2(son[x], tp);
for (int i : v[x]) {
if (i == fa[x] || i == son[x]) continue;
dfs2(i, i);
}
}
void modify_add(int x, int y, int z) { // 实现修改权值。
while (top[x] != top[y]) {
if (dep[top[x]] > dep[top[y]]) {
seg.modify(1, z, dfn[top[x]], dfn[x]);
x = fa[top[x]];
} else {
seg.modify(1, z, dfn[top[y]], dfn[y]);
y = fa[top[y]];
}
}
if (dep[x] < dep[y]) {
seg.modify(1, z, dfn[x], dfn[y]);
} else {
seg.modify(1, z, dfn[y], dfn[x]);
}
}
int query_sum(int x, int y) { // 实现查询。
int res = 0;
while (top[x] != top[y]) {
if (dep[top[x]] > dep[top[y]]) {
(res += seg.query(1, dfn[top[x]], dfn[x])) %= p;
x = fa[top[x]];
} else {
(res += seg.query(1, dfn[top[y]], dfn[y])) %= p;
y = fa[top[y]];
}
}
if (dep[x] < dep[y]) {
(res += seg.query(1, dfn[x], dfn[y])) %= p;
} else {
(res += seg.query(1, dfn[y], dfn[x])) %= p;
}
return res;
}
signed main() {
read(n, m, s, p);
for (int i = 1; i <= n; i++) {
read(w[i]);
}
for (int i = 1, x, y; i < n; i++) {
read(x, y);
v[x].pb(y), v[y].pb(x);
}
dfs1(s, N - 1);
dfs2(s, s);
seg.build(1, 1, n);
while (m--) {
int op, x, y, z;
read(op);
if (op == 1) {
read(x, y, z);
modify_add(x, y, z);
} else if (op == 2) {
read(x, y);
printl(query_sum(x, y) % p);
} else if (op == 3) {
read(x, z);
seg.modify(1, z, dfn[x], dfn[x] + siz[x] - 1);
} else {
read(x);
printl(seg.query(1, dfn[x], dfn[x] + siz[x] - 1) % p);
}
}
return 0;
}
3.2 习题2
题目大意:
给定一个树形结构,初始所有节点权值都为 0。接下来进行若干次操作,若将 \(x\) 值改为 1,则需要将根至 \(x\) 的权值都改为 1。若改为 0,则其子树都将变为 0。求问每次操作会有多少个点权值发生变化。
思路:
还是模板,记录一下原来的权值,再与修改之后的权值对比即可。
参考代码(部分):
const int N = 1e5 + 5, p = 1e9 + 7;
int n, m, son[N], dfn[N], rnk[N], top[N], siz[N], dep[N], fa[N], idx;
vector<int> v[N];
struct SegmentTree {
struct Tree {
int l, r, x, add;
} tr[N << 2];
void pushup(int u) {
tr[u].x = tr[u << 1].x + tr[u << 1 | 1].x;
}
void pushdown(int u) {
Tree &root = tr[u], &left = tr[u << 1], &right = tr[u << 1 | 1];
if (~root.add) {
(left.add = root.add) %= p;
(left.x = (left.r - left.l + 1) * root.add) %= p;
(right.add = root.add) %= p;
(right.x = (right.r - right.l + 1) * root.add) %= p;
root.add = -1;
}
}
void build(int u, int l, int r) {
tr[u].l = l, tr[u].r = r, tr[u].add = 0;
if (l == r) {
tr[u].x = 0;
return;
}
int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
pushup(u);
}
int query(int u, int l, int r) {
if (tr[u].l >= l && tr[u].r <= r) return tr[u].x;
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1, res = 0;
if (mid >= l) (res += query(u << 1, l, r)) %= p;
if (mid < r) (res += query(u << 1 | 1, l, r)) %= p;
return res;
}
void modify(int u, int d, int l, int r) {
if (tr[u].l >= l && tr[u].r <= r) {
(tr[u].x = (tr[u].r - tr[u].l + 1) * d) %= p;
(tr[u].add = d) %= p;
} else {
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) modify(u << 1, d, l, r);
if (r > mid) modify(u << 1 | 1, d, l, r);
pushup(u);
}
}
} seg;
void dfs1(int x, int f) {
siz[x] = 1, dep[x] = dep[f] + 1, fa[x] = f;
son[x] = -1;
for (int i : v[x]) {
if (i == f) continue;
dfs1(i, x);
siz[x] += siz[i];
if (son[x] == -1 || siz[i] > siz[son[x]]) son[x] = i;
}
}
void dfs2(int x, int tp) {
top[x] = tp;
dfn[x] = ++idx, rnk[idx] = x;
if (son[x] == -1) return;
dfs2(son[x], tp);
for (int i : v[x]) {
if (i == fa[x] || i == son[x]) continue;
dfs2(i, i);
}
}
void modify_set(int x, int y, int z) {
while (top[x] != top[y]) {
if (dep[top[x]] > dep[top[y]]) {
seg.modify(1, z, dfn[top[x]], dfn[x]);
x = fa[top[x]];
} else {
seg.modify(1, z, dfn[top[y]], dfn[y]);
y = fa[top[y]];
}
}
if (dep[x] < dep[y]) {
seg.modify(1, z, dfn[x], dfn[y]);
} else {
seg.modify(1, z, dfn[y], dfn[x]);
}
}
int query_sum(int x, int y) {
int res = 0;
while (top[x] != top[y]) {
if (dep[top[x]] > dep[top[y]]) {
(res += seg.query(1, dfn[top[x]], dfn[x])) %= p;
x = fa[top[x]];
} else {
(res += seg.query(1, dfn[top[y]], dfn[y])) %= p;
y = fa[top[y]];
}
}
if (dep[x] < dep[y]) {
(res += seg.query(1, dfn[x], dfn[y])) %= p;
} else {
(res += seg.query(1, dfn[y], dfn[x])) %= p;
}
return res;
}
signed main() {
read(n);
for (int i = 1, x; i < n; i++) {
read(x);
v[i + 1].pb(x + 1), v[x + 1].pb(i + 1);
}
dfs1(1, N - 1);
dfs2(1, 1);
seg.build(1, 1, n + 1);
read(m);
while (m--) {
string s;
int x;
read(s), read(x);
if (s[0] == 'i') {
int oldsum = query_sum(1, x + 1);
modify_set(1, x + 1, 1);
printl(query_sum(1, x + 1) - oldsum);
} else {
int oldsum = seg.query(1, dfn[x + 1], dfn[x + 1] + siz[x + 1] - 1);
seg.modify(1, 0, dfn[x + 1], dfn[x + 1] + siz[x + 1] - 1);
printl(oldsum);
}
}
return 0;
}
3.3 其它习题
无所谓,反正我认为是双倍经验。
尝试使用树剖实现。
相比于模板要多维护几个值域,转换有一定思维难度。
The End
树剖感觉和线段树离不开,玩来玩去核心思路还是不变的。有问题请指出,谢谢。
Thanks
暂无
标签:剖分,fa,int,top,笔记,seg,dep,dfn,重链 From: https://www.cnblogs.com/Pretharp/p/zhong_lian_pou_fen.html