参考资料:
树链剖分的含义
树链剖分就是将一棵树划分成若干条链,并保证每一条链上的dfs序连续,从而利用线段树对树上的信息进行维护。
重链剖分
实现
fa[i]
:结点i
的父结点的编号。siz[i]
:以结点i
为根的子树的结点数。dep[i]
:结点i
的深度。son[i]
:结点i
的所有子结点中,siz
值最大的子结点的编号。dfn[i]
:结点i
的dfs序。top[i]
:结点i
所在链中,dep
值最小的结点的编号。rnk[i]
:dfs序为i
的结点的编号。
重链剖分就是要得到每个结点的上述信息,需要通过两个dfs来实现:
void dfs1(int x){ //得到fa、siz、dep、son
siz[x] = 1;
son[x] = 0;
for(int i=head[x];i!=0;i=edge[i].next){
int to = edge[i].to;
if(to==fa[x]) continue;
fa[to] = x;
dep[to] = dep[x]+1;
dfs1(to);
siz[x] += siz[to];
if(siz[to]>siz[son[x]]) son[x] = to;
}
}
void dfs2(int x, int rt){ //得到dfn、top、rnk
index++;
dfn[x] = index;
top[x] = rt;
rnk[index] = x;
if(son[x]!=0) dfs2(son[x], rt);
for(int i=head[x];i!=0;i=edge[i].next){
int to = edge[i].to;
if(to==fa[x] || to==son[x]) continue;
dfs2(to, to);
}
}
路径上维护
注:代码中的modify()
和query
都是线段树的操作,返回值均为void。
void modify_tree1(int x, int y, int v){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x, y); //易错!
modify(1, dfn[top[x]], dfn[x], v);
x = fa[top[x]];
}
if(dep[x]<dep[y]) swap(x, y);
modify(1, dfn[y], dfn[x], v);
}
int query_tree1(int x, int y){
int res = 0;
while(top[x]!=top[y]){ //直到两结点位于同一条链上为止
if(dep[top[x]]<dep[top[y]]) swap(x, y);
ans = 0;
query(1, dfn[top[x]], dfn[x]); //query()将结果保存在全局变量ans中
res += ans;
x = fa[top[x]];
}
if(dep[x]<dep[y]) swap(x, y);
ans = 0;
query(1, dfn[y], dfn[x]);
res += ans;
return res;
}
子树上维护
子树上的结点的dfs序是连续的。
void modify_tree2(int x, int v){
modify(1, dfn[x], dfn[x]+siz[x]-1, v);
}
int query_tree2(int x){
ans = 0;
query(1, dfn[x], dfn[x]+siz[x]-1);
return ans;
}
LCA
int LCA(int x, int y){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x, y);
x = fa[top[x]];
}
if(dep[x]<dep[y]) swap(x, y);
return y;
}
例1 P2590 [ZJOI2008]树的统计
题目描述
一棵树上有 n n n 个节点,编号分别为 1 1 1 到 n n n,每个节点都有一个权值 w w w。
我们将以下面的形式来要求你对这棵树完成一些操作:
I. CHANGE u t
: 把结点
u
u
u 的权值改为
t
t
t。
II. QMAX u v
: 询问从点
u
u
u 到点
v
v
v 的路径上的节点的最大权值。
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 和 b b b,表示节点 a a a 和节点 b b b 之间有一条边相连。
接下来一行 n n n 个整数,第 i i i 个整数 w i w_i wi 表示节点 i i i 的权值。
接下来 1 1 1 行,为一个整数 q q q,表示操作的总数。
接下来
q
q
q 行,每行一个操作,以 CHANGE u t
或者 QMAX u v
或者 QSUM u v
的形式给出。
输出格式
对于每个 QMAX
或者 QSUM
的操作,每行输出一个整数表示要求输出的结果。
样例 #1
样例输入 #1
4
1 2
2 3
4 1
4 2 1 3
12
QMAX 3 4
QMAX 3 3
QMAX 3 2
QMAX 2 3
QSUM 3 4
QSUM 2 1
CHANGE 1 5
QMAX 3 4
CHANGE 3 6
QMAX 3 4
QMAX 2 4
QSUM 3 4
样例输出 #1
4
1
2
2
10
6
5
6
5
16
提示
对于 100 % 100 \% 100% 的数据,保证 1 ≤ n ≤ 3 × 1 0 4 1\le n \le 3\times 10^4 1≤n≤3×104, 0 ≤ q ≤ 2 × 1 0 5 0\le q\le 2\times 10^5 0≤q≤2×105。
中途操作中保证每个节点的权值 w w w 在 − 3 × 1 0 4 -3\times 10^4 −3×104 到 3 × 1 0 4 3\times 10^4 3×104 之间。
思路
本题为树剖路径上维护与查询的模板题。
代码
#include<bits/stdc++.h>
#define lc (i<<1)
#define rc (i<<1|1)
#define inf 0x7fffffff
using namespace std;
const int maxn = 3e4+5;
int n, q;
int a[maxn];
struct EDGE{
int to;
int next;
};
EDGE edge[maxn<<1];
int tot = 0;
int head[maxn];
void addEdge(int fr, int to){
tot++;
edge[tot].to = to;
edge[tot].next = head[fr];
head[fr] = tot;
}
int siz[maxn], dep[maxn], son[maxn], dfn[maxn], f[maxn], top[maxn], nfd[maxn];
int cnt = 0;
void dfs1(int x, int fa){
siz[x] = 1;
son[x] = 0;
f[x] = fa;
dep[x] = dep[fa]+1;
for(int i=head[x];i!=0;i=edge[i].next){
int to = edge[i].to;
if(to == fa) continue;
dfs1(to, x);
siz[x] += siz[to];
if(siz[to]>siz[son[x]]) son[x] = to;
}
}
void dfs2(int x, int rt){
cnt++;
dfn[x] = cnt;
nfd[cnt] = x;
top[x] = rt;
if(son[x] != 0) dfs2(son[x], rt);
for(int i=head[x];i!=0;i=edge[i].next){
int to = edge[i].to;
if(to == f[x] || to == son[x]) continue;
dfs2(to, to);
}
}
struct TREE{
int l, r;
int M;
int sum;
};
TREE tree[maxn<<2];
void pushUp(int i){
tree[i].M = max(tree[lc].M, tree[rc].M);
tree[i].sum = tree[lc].sum + tree[rc].sum;
}
void build(int i, int l, int r){
tree[i].l = l, tree[i].r = r;
if(l==r){
tree[i].M = a[nfd[l]]; //易错
tree[i].sum = a[nfd[l]];
return;
}
int mid = (l+r)>>1;
build(lc, l, mid);
build(rc, mid+1, r);
pushUp(i);
}
void modify(int i, int l, int r, int v){
if(tree[i].l>=l&&tree[i].r<=r){
tree[i].M = v;
tree[i].sum = v;
}
else if(tree[i].r<l||tree[i].l>r){
return;
}
else{
modify(lc, l, r, v);
modify(rc, l, r, v);
pushUp(i);
}
}
int ans = 0;
void queryMax(int i, int l, int r){
if(tree[i].l>=l&&tree[i].r<=r){
ans = max(ans, tree[i].M);
}
else if(tree[i].r<l||tree[i].l>r){
return;
}
else{
queryMax(lc, l, r);
queryMax(rc, l, r);
}
}
int queryMaxOnTree(int u, int v){
int res = -inf;
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]]) swap(u, v);
ans = -inf;
queryMax(1, dfn[top[u]], dfn[u]); //易错:链头的dfs序最小
res = max(res, ans);
u = f[top[u]];
}
if(dep[u]<dep[v]) swap(u, v);
ans = -inf;
queryMax(1, dfn[v], dfn[u]);
res = max(res, ans);
return res;
}
void querySum(int i, int l, int r){
if(tree[i].l>=l&&tree[i].r<=r){
ans += tree[i].sum;
}
else if(tree[i].r<l||tree[i].l>r){
return;
}
else{
querySum(lc, l, r);
querySum(rc, l, r);
}
}
int querySumOnTree(int u, int v){
int res = 0;
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]]) swap(u, v);
ans = 0;
querySum(1, dfn[top[u]], dfn[u]);
res += ans;
u = f[top[u]];
}
if(dep[u]<dep[v]) swap(u, v);
ans = 0;
querySum(1, dfn[v], dfn[u]);
res += ans;
return res;
}
int main(){
cin>>n;
for(int i=1;i<n;i++){
int u, v;
cin>>u>>v;
addEdge(u, v);
addEdge(v, u);
}
for(int i=1;i<=n;i++){
cin>>a[i];
}
dfs1(1, 0);
dfs2(1, 1);
build(1, 1, n);
cin>>q;
for(int i=1;i<=q;i++){
string cmd;
int u, v;
cin>>cmd>>u>>v;
if(cmd[1]=='H') modify(1, dfn[u], dfn[u], v);
else if(cmd[1]=='M') cout<<queryMaxOnTree(u, v)<<'\n';
else cout<<querySumOnTree(u, v)<<'\n';
}
return 0;
}
例2 P4315 月下“毛景树”
题目背景
毛毛虫经过及时的变形,最终逃过的一劫,离开了菜妈的菜园。 毛毛虫经过千山万水,历尽千辛万苦,最后来到了小小的绍兴一中的校园里。
题目描述
爬啊爬爬啊爬毛毛虫爬到了一颗小小的“毛景树”下面,发现树上长着他最爱吃的毛毛果~~ “毛景树”上有 N N N 个节点和 N − 1 N-1 N−1 条树枝,但节点上是没有毛毛果的,毛毛果都是长在树枝上的。但是这棵“毛景树”有着神奇的魔力,他能改变树枝上毛毛果的个数:
Change k w
:将第k条树枝上毛毛果的个数改变为 w w w 个。Cover u v w
:将节点 u u u 与节点 v v v 之间的树枝上毛毛果的个数都改变为 w w w 个。Add u v w
:将节点 u u u 与节点 v v v 之间的树枝上毛毛果的个数都增加 w w w 个。
由于毛毛虫很贪,于是他会有如下询问:
Max u v
:询问节点 u u u 与节点 v v v 之间树枝上毛毛果个数最多有多少个。
输入格式
第一行一个正整数 N N N。
接下来
N
−
1
N-1
N−1 行,每行三个正整数
U
i
,
V
i
U_i,V_i
Ui,Vi 和
W
i
W_i
Wi,第
i
+
1
i+1
i+1 行描述第
i
i
i 条树枝。表示第
i
i
i 条树枝连接节点
U
i
U_i
Ui 和节点
V
i
V_i
Vi,树枝上有
W
i
W_i
Wi 个毛毛果。 接下来是操作和询问,以 Stop
结束。
输出格式
对于毛毛虫的每个询问操作,输出一个答案。
样例 #1
样例输入 #1
4
1 2 8
1 3 7
3 4 9
Max 2 4
Cover 2 4 5
Add 1 4 10
Change 1 16
Max 2 4
Stop
样例输出 #1
9
16
提示
对于全部数据, 1 ≤ N ≤ 1 0 5 1\le N\le 10^5 1≤N≤105,操作和询问数目不超过 1 0 5 10^5 105。
保证在任意时刻,所有树枝上毛毛果的个数都不会超过 1 0 9 10^9 109 个。
思路
本题为树剖维护边权的模板题,还涉及到了线段树的染色+修改,需要注意的细节较多。
代码
#include<bits/stdc++.h>
#define int long long
#define lc (i<<1)
#define rc (i<<1|1)
using namespace std;
const int maxn = 1e5+5;
struct NODE{
int fr, to;
int v;
};
NODE node[maxn];
struct EDGE{
int fr, to, v;
int next;
};
EDGE edge[maxn<<1];
int head[maxn];
int tot = 0;
void addEdge(int fr, int to, int v){
tot++;
edge[tot].to = to;
edge[tot].v = v;
edge[tot].next = head[fr];
head[fr] = tot;
}
struct TREE{
int l, r;
int m;
int coverTag = -1;
int addTag = 0;
};
TREE tree[maxn<<2];
int siz[maxn], fa[maxn], son[maxn], dep[maxn], dfn[maxn], top[maxn], ndf[maxn];
int a[maxn]; //用点权代替边权
int cnt = 0;
void dfs1(int x, int f){
siz[x] = 1;
son[x] = 0;
fa[x] = f;
dep[x] = dep[f]+1;
for(int i=head[x];i!=0;i=edge[i].next){
int to = edge[i].to, v = edge[i].v;
if(to==f) continue;
a[to] = v;
dfs1(to, x);
siz[x] += siz[to];
if(siz[to]>siz[son[x]]) son[x] = to;
}
}
void dfs2(int x, int rt){
cnt++;
dfn[x] = cnt;
ndf[cnt] = x;
top[x] = rt;
if(son[x]!=0) dfs2(son[x], rt);
for(int i=head[x];i!=0;i=edge[i].next){
int to = edge[i].to;
if(to==fa[x] || to==son[x]) continue;
dfs2(to, to);
}
}
void pushUp(int i){
tree[i].m = max(tree[lc].m, tree[rc].m);
}
void pushDown(int i){ //注意pushdown顺序
if(tree[i].coverTag!=-1){
tree[lc].addTag = tree[rc].addTag = 0;
tree[lc].m = tree[rc].m = tree[i].coverTag;
tree[lc].coverTag = tree[rc].coverTag = tree[i].coverTag;
tree[i].coverTag = -1;
}
if(tree[i].addTag!=0){
tree[lc].addTag += tree[i].addTag;
tree[lc].m += tree[i].addTag;
tree[rc].addTag += tree[i].addTag;
tree[rc].m += tree[i].addTag;
tree[i].addTag = 0;
}
}
void build(int i, int l, int r){
tree[i].l = l, tree[i].r = r;
if(l==r){
tree[i].m = a[ndf[l]];
return;
}
int mid = (l+r)>>1;
build(lc, l, mid);
build(rc, mid+1, r);
pushUp(i);
}
void cover(int i, int l, int r, int v){
if(tree[i].l>=l&&tree[i].r<=r){
tree[i].coverTag = v;
tree[i].addTag = 0;
tree[i].m = v;
}
else if(tree[i].l>r||tree[i].r<l){
return;
}
else{
pushDown(i);
cover(lc, l, r, v);
cover(rc, l, r, v);
pushUp(i);
}
}
void add(int i, int l, int r, int v){
if(tree[i].l>=l&&tree[i].r<=r){
tree[i].addTag += v;
tree[i].m += v;
}
else if(tree[i].l>r||tree[i].r<l){
return;
}
else{
pushDown(i);
add(lc, l, r, v);
add(rc, l, r, v);
pushUp(i);
}
}
void modifyOnTree(int x, int y, int v, int cmd){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x, y);
if(cmd==0) cover(1, dfn[top[x]], dfn[x], v); //易错
else add(1, dfn[top[x]], dfn[x], v);
x = fa[top[x]];
}
if(x==y) return;
if(dep[x]<dep[y]) swap(x, y);
if(cmd==0) cover(1, dfn[y]+1, dfn[x], v); //易错
else add(1, dfn[y]+1, dfn[x], v);
}
int ans = 0;
void query(int i, int l ,int r){
if(tree[i].l>=l&&tree[i].r<=r){
ans = max(ans, tree[i].m);
}
else if(tree[i].l>r||tree[i].r<l){
return;
}
else{
pushDown(i);
query(lc, l, r);
query(rc, l, r);
}
}
int queryOnTree(int x, int y){
int res = 0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x, y);
ans = 0;
query(1, dfn[top[x]], dfn[x]);
res = max(res, ans);
x = fa[top[x]];
}
if(x==y) return res;
if(dep[x]<dep[y]) swap(x, y);
ans = 0;
query(1, dfn[y]+1, dfn[x]);
res = max(res, ans);
return res;
}
int n;
signed main(){
cin>>n;
for(int i=1;i<n;i++){
int u, v, w;
cin>>u>>v>>w;
node[i].fr = u, node[i].to = v, node[i].v = w;
addEdge(u, v, w);
addEdge(v, u, w);
}
dfs1(1, 0);
dfs2(1, 1);
build(1, 1, n);
while(1){
string cmd;
int u, v, w;
cin>>cmd;
if(cmd[0]=='S') break;
else if(cmd[0]=='M'){
cin>>u>>v;
cout<<queryOnTree(u, v)<<'\n';
}
else if(cmd[0]=='A'){
cin>>u>>v>>w;
modifyOnTree(u, v, w, 1);
}
else{
if(cmd[1]=='o'){
cin>>u>>v>>w;
modifyOnTree(u, v, w, 0);
}
else{
cin>>u>>v;
modifyOnTree(node[u].fr, node[u].to, v, 0);
}
}
}
return 0;
}
例3 P1967 [NOIP2013 提高组] 货车运输
题目描述
A 国有 n n n 座城市,编号从 1 1 1 到 n n n,城市之间有 m m m 条双向道路。每一条道路对车辆都有重量限制,简称限重。
现在有 q q q 辆货车在运输货物, 司机们想知道每辆车在不超过车辆限重的情况下,最多能运多重的货物。
输入格式
第一行有两个用一个空格隔开的整数 $ n,m$,表示 A 国有 $ n$ 座城市和 m m m 条道路。
接下来
m
m
m 行每行三个整数
x
,
y
,
z
x, y, z
x,y,z,每两个整数之间用一个空格隔开,表示从
x
x
x 号城市到
y
y
y 号城市有一条限重为
z
z
z 的道路。
注意:
x
≠
y
x \neq y
x=y,两座城市之间可能有多条道路 。
接下来一行有一个整数 q q q,表示有 q q q 辆货车需要运货。
接下来 q q q 行,每行两个整数 x , y x,y x,y,之间用一个空格隔开,表示一辆货车需要从 x x x 城市运输货物到 y y y 城市,保证 x ≠ y x \neq y x=y
输出格式
共有
q
q
q 行,每行一个整数,表示对于每一辆货车,它的最大载重是多少。
如果货车不能到达目的地,输出
−
1
-1
−1。
样例 #1
样例输入 #1
4 3
1 2 4
2 3 3
3 1 1
3
1 3
1 4
1 3
样例输出 #1
3
-1
3
提示
对于 100 % 100\% 100% 的数据, 1 ≤ n < 1 0 4 1 \le n < 10^4 1≤n<104, 1 ≤ m < 5 × 1 0 4 1 \le m < 5\times 10^4 1≤m<5×104, 1 ≤ q < 3 × 1 0 4 1 \le q< 3\times 10^4 1≤q<3×104, 0 ≤ z ≤ 1 0 5 0 \le z \le 10^5 0≤z≤105。
思路
首先使用 Kruskal 算法求出最大生成树,如果最后得到是一片森林,则在森林之间建立权值为-1
的边,使之成为一棵树。然后利用树剖算法对最大生成树进行划分,并将边的权值保存在深度较大的点上。利用线段树维护区间最小值,即可得到正确答案。
代码
#include<bits/stdc++.h>
#define INF 0x7fffffff
#define lc (i<<1)
#define rc (i<<1|1)
using namespace std;
const int maxn = 1e4+5;
const int maxm = 5e4+5;
const int maxq = 3e4+5;
int n, m, q;
int x, y, z;
struct NODE{
int x, y, z;
bool operator<(const NODE& a)const{
return z>a.z; //sort默认升序排序
}
};
NODE node[maxm];
struct EDGE{
int to;
int v;
int next;
};
EDGE edge[maxm<<1];
int head[maxn];
int tot = 0;
void addEdge(int fr, int to, int v){
tot++;
edge[tot].to = to;
edge[tot].v = v;
edge[tot].next = head[fr];
head[fr] = tot;
}
int fa[maxn], rnk[maxn];
void init(int n){
for(int i=1;i<=n;i++){
fa[i] = i;
rnk[i] = 1;
}
}
int find(int x){
return (x==fa[x]) ? x : (fa[x]=find(fa[x]));
}
void merge(int x, int y){
int X = find(x), Y = find(y);
if(X==Y) return;
if(rnk[X]>rnk[Y]) fa[Y] = X;
else if(rnk[Y]>rnk[X]) fa[X] = Y;
else{
fa[Y] = X;
rnk[X]++;
}
}
bool flag[maxn];
vector<int> temp;
void buildTree(){
for(int i=1;i<=m;i++){
int fr=node[i].x, to=node[i].y, v=node[i].z;
if(find(fr)!=find(to)){
addEdge(fr, to, v);
addEdge(to, fr, v);
merge(fr, to);
}
}
for(int i=1;i<=n;i++){
int I = find(i);
if(!flag[I]){
temp.push_back(I);
flag[I] = 1;
}
}
for(int j=1;j<temp.size();j++){
addEdge(temp[0], temp[j], -1);
addEdge(temp[j], temp[0], -1);
// merge(i, j);
}
}
int siz[maxn], son[maxn], dep[maxn], dfn[maxn], f[maxn], top[maxn], value[maxn], nfd[maxn];
int idx = 0;
void dfs1(int x, int fa){
f[x] = fa;
dep[x] = dep[fa]+1;
siz[x] = 1;
son[x] = 0;
for(int i=head[x];i!=0;i=edge[i].next){
int to=edge[i].to, v=edge[i].v;
if(to==fa) continue;
dfs1(to, x);
value[to] = v; //将边权保存在深度较大的点上
siz[x] += siz[to];
if(siz[to]>siz[son[x]]) son[x] = to;
}
}
void dfs2(int x, int rt){
idx++;
dfn[x] = idx;
nfd[idx] = x;
top[x] = rt;
if(son[x]!=0) dfs2(son[x], rt);
for(int i=head[x];i!=0;i=edge[i].next){
int to=edge[i].to, v=edge[i].v;
if(to==f[x] || to==son[x]) continue;
dfs2(to, to);
}
}
struct TREE{
int l, r;
int v;
};
TREE tree[maxn<<2];
void pushUp(int i){
tree[i].v = min(tree[lc].v, tree[rc].v);
}
void build(int i, int l, int r){
tree[i].l = l, tree[i].r = r;
if(l==r){
tree[i].v = value[nfd[l]]; //线段树中的l、r对应的是dfs序
return;
}
int mid = (l+r)>>1;
build(lc, l, mid);
build(rc, mid+1, r);
pushUp(i);
}
int ans = INF;
void query(int i, int l, int r){
if(tree[i].l>=l&&tree[i].r<=r){
ans = min(ans, tree[i].v);
}
else if(tree[i].l>r||tree[i].r<l) return;
else{
query(lc, l, r);
query(rc, l, r);
}
}
int queryTree(int x, int y){
int rtn = INF;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x, y);
ans = INF;
query(1, dfn[top[x]], dfn[x]);
rtn = min(ans, rtn);
x = f[top[x]];
}
if(dep[x]<dep[y]) swap(x, y);
ans = INF;
query(1, dfn[y]+1, dfn[x]);
rtn = min(ans, rtn);
return rtn;
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>node[i].x>>node[i].y>>node[i].z;
}
sort(node+1, node+m+1);
init(n);
buildTree();
value[1] = INF;
dfs1(1, 0);
dfs2(1, 1);
build(1, 1, n);
cin>>q;
for(int i=1;i<=q;i++){
int x, y;
cin>>x>>y;
if(find(x)!=find(y)) cout<<-1;
else cout<<queryTree(x, y);
cout<<'\n';
}
return 0;
}