树链剖分真的不难,基础够了,简直秒懂
1、前置知识
线段树、树状数组等,dfs序,lca
2、引入
首先先放树剖模板题
四个操作:
- 1 x y z 表示将树从x到y结点最短路径上所有节点的值都加上z
- 2 x y 表示求树从x到y结点最短路径上所有节点的值之和
- 3 x z 表示将以x为根节点的子树内所有节点值都加上z
- 4 x 表示求以x为根节点的子树内所有节点值之和
先倒退一下难度,如果只有询问呢?是不是树上倍增就可以了
但是加上修改呢?st数组就没能力了,所以我们需要一种更优的算法来解决
那就是,树链剖分
引入概念
重儿子:父亲节点的所有儿子中子树结点数目最多(size最大)的结点;
轻儿子:父亲节点中除了重儿子以外的儿子;
重边:父亲结点和重儿子连成的边;
轻边:父亲节点和轻儿子连成的边;
重链:由多条重边连接而成的路径;
轻链:由多条轻边连接而成的路径;
如图,黄色为重儿子,红色为重边
下图中,
重链为:3->20;4->12;11->11
轻链为:1->2;7->7;5->5;9->9;15->15;16->16;19->19
这样一颗树,就是树链剖分的树
那么,怎么弄出这样一颗树呢
3、变量
有一些经常使用的数组
- size[]数组,用来保存以x为根的子树节点个数
- son[]数组,用来保存重儿子
- fa[]数组,用来保存当前节点的父亲
- d[]数组,用来保存当前节点的深度
- id[]数组,用来保存树中每个节点dfs序中的新编号
- top[]数组,用来保存当前节点的所在链的顶端节点
- rk[]数组,用来保存当前节点在线段树中的位置
4、dfs
重点,树剖必备dfs,跑两遍
- 第一遍,处理出size,son,d,fa
void dfs1(int u, int pre){//pre记录父亲
d[u] = d[pre] + 1, fa[u] = pre;//处理d、fa数组
size[u] = 1, son[u] = -1;//初始只有一个点,没有重儿子
for (int i = head[u]; i; i = edge[i].next){//枚举儿子
int v = edge[i].to;
if (v != pre){
dfs1(v, u);
size[u] += size[v];//统计size
if (son[u] == -1 || son[u] != -1 && size[v] > size[son[u]]) son[u] = v;//更新son数组
}
}
}
- 第二遍,处理出id,top,rk数组
void dfs2(int u, int x){//x为链上顶端点
id[u] = ++cnt, wt[cnt] = w[u] % qy, top[u] = x;//id数组dfs序,这里用wt数组存新编号对应原来的点的值
if (son[u] == -1) return;//没有重儿子,退出
dfs2(son[u], x);//首先遍历重儿子
for (int i = head[u]; i; i = edge[i].next){
int v = edge[i].to;
if (v != fa[u] && v != son[u]) dfs2(v, v);//轻儿子所在的链的顶端是自己
}
}
5、用树剖求lca
可以用树剖求lca,单纯的树剖求lca比树上倍增快得多
具体采取跳重链的做法:
点x,y,求lca(x,y)
- 若x、y不在同一条重链上,把两个点中top的深度更深的那个点往上跳,跳过它属于的那条链,跳到它属于的那条链的顶端的父亲
不断跳链,直到两个点属于同一条重链为止 - 若x、y属于同一条重链,那么lca就是两个点中深度更浅的那个
具体Code(手码):
while (top[x] != top[y]){//两点不在同一条重链上
if (d[top[x]] < d[top[y]]) swap(x, y);//x为top的深度更深的那个点
x = fa[top[x]];//x跳到它属于的那条链的顶端的父亲
}
lca = d[x] < d[y] ? x : y;//lca就是两个点中深度更浅的那个
6、维护数据
我们用数据结构来维护数据
这里用线段树
假如维护两点最短路径上的点进行区间加
仿照树剖求lca的做法
点跳链之前,先把该点到该点属于的重链的顶端那一条小路径的值区间加,然后再跳链
Code:
void update_range(int x, int y, int delta){
delta %= qy;
while (top[x] != top[y]){
if (d[top[x]] < d[top[y]]) swap(x, y);
update(1, id[top[x]], id[x], delta);
//至于为什么可以直接进行区间维护,这就和dfs2有关了,
//因为我们总是优先遍历重儿子,所以一条重链上的点的dfs序(id数组)一定是连续的
x = fa[top[x]];
}
if (d[x] < d[y]) swap(x, y);
update(1, id[y], id[x], delta);
}
那么怎么维护子树的区间加呢
发现从某个点开始,遍历它的儿子,从这个点开始的id数组总是连续的
这个点的id是id[x],那么以该点为根的子树中的最后一个节点的id一定是id[x]+size[x]-1
因为我们并不知道那个点到底是谁,但是我们可以通过x算出来
所以,
Code:
void update_size(int x, int delta){//没啥好解释了吧
delta %= qy;
update(1, id[x], id[x] + size[x] - 1, delta);
}
求和操作呢,只要把update改成query就行了
7、例题讲解
树剖模板题
四大操作,码量较大
得注意时刻取模
Code:
#include <bits/stdc++.h>
#define maxn 100010
#define ls rt << 1
#define rs rt << 1 | 1
using namespace std;
struct Edge{
int to, next;
}edge[maxn << 1];
struct Seg{
int l, r, sum, tag;
}seg[maxn << 2];
int n, m, r, qy, num, cnt, head[maxn << 1], d[maxn], fa[maxn], size[maxn], son[maxn], top[maxn], id[maxn], w[maxn], wt[maxn];
inline int read(){
int s = 0, w = 1;
char c = getchar();
for (; !isdigit(c); c = getchar()) if (c == '-') w = -1;
for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
return s * w;
}
void add_edge(int x, int y){ edge[++num].to = y; edge[num].next = head[x]; head[x] = num; }
void pushup(int rt){ seg[rt].sum = (seg[ls].sum + seg[rs].sum) % qy; }
void pushdown(int rt){
seg[ls].sum = (seg[ls].sum + (seg[ls].r - seg[ls].l + 1) * seg[rt].tag) % qy;
seg[rs].sum = (seg[rs].sum + (seg[rs].r - seg[rs].l + 1) * seg[rt].tag) % qy;
seg[ls].tag = (seg[ls].tag + seg[rt].tag) % qy;
seg[rs].tag = (seg[rs].tag + seg[rt].tag) % qy;
seg[rt].tag = 0;
}
void build(int rt, int l, int r){
seg[rt].l = l, seg[rt].r = r;
if (l == r){
seg[rt].sum = wt[l];
return;
}
int mid = (l + r) >> 1;
build(ls, l, mid); build(rs, mid + 1, r);
pushup(rt);
}
void update(int rt, int l, int r, int delta){
if (seg[rt].l > r || seg[rt].r < l) return;
if (seg[rt].l >= l && seg[rt].r <= r){
seg[rt].sum = (seg[rt].sum + (seg[rt].r - seg[rt].l + 1) * delta) % qy;
seg[rt].tag = (seg[rt].tag + delta) % qy;
return;
}
if (seg[rt].tag) pushdown(rt);
update(ls, l, r, delta); update(rs, l, r, delta);
pushup(rt);
}
int query(int rt, int l, int r){
if (seg[rt].l > r || seg[rt].r < l) return 0;
if (seg[rt].l >= l && seg[rt].r <= r) return seg[rt].sum;
if (seg[rt].tag) pushdown(rt);
return (query(ls, l, r) + query(rs, l, r)) % qy;
}
void update_range(int x, int y, int delta){
delta %= qy;
while (top[x] != top[y]){
if (d[top[x]] < d[top[y]]) swap(x, y);
update(1, id[top[x]], id[x], delta);
x = fa[top[x]];
}
if (d[x] < d[y]) swap(x, y);
update(1, id[y], id[x], delta);
}
int query_range(int x, int y){
int ans = 0;
while (top[x] != top[y]){
if (d[top[x]] < d[top[y]]) swap(x, y);
ans = (ans + query(1, id[top[x]], id[x])) % qy;
x = fa[top[x]];
}
if (d[x] < d[y]) swap(x, y);
return (ans + query(1, id[y], id[x])) % qy;
}
void update_size(int x, int delta){
delta %= qy;
update(1, id[x], id[x] + size[x] - 1, delta);
}
int query_size(int x){ return query(1, id[x], id[x] + size[x] - 1); }
void dfs1(int u, int pre){
d[u] = d[pre] + 1, fa[u] = pre;
size[u] = 1, son[u] = -1;
for (int i = head[u]; i; i = edge[i].next){
int v = edge[i].to;
if (v != pre){
dfs1(v, u);
size[u] += size[v];
if (son[u] == -1 || son[u] != -1 && size[v] > size[son[u]]) son[u] = v;
}
}
}
void dfs2(int u, int x){
id[u] = ++cnt, wt[cnt] = w[u] % qy, top[u] = x;
if (son[u] == -1) return;
dfs2(son[u], x);
for (int i = head[u]; i; i = edge[i].next){
int v = edge[i].to;
if (v != fa[u] && v != son[u]) dfs2(v, v);
}
}
int main(){
n = read(), m = read(), r = read(), qy = read();
for (int i = 1; i <= n; ++i) w[i] = read();
for (int i = 1; i < n; ++i){
int x = read(), y = read();
add_edge(x, y); add_edge(y, x);
}
dfs1(r, 0);
dfs2(r, r);
build(1, 1, n);
while (m--){
int opt = read();
if (opt == 1){
int x = read(), y = read(), z = read();
update_range(x, y, z);
}
if (opt == 2){
int x = read(), y = read();
printf("%d\n", query_range(x, y));
}
if (opt == 3){
int x = read(), y = read();
update_size(x, y);
}
if (opt == 4){
int x = read();
printf("%d\n", query_size(x));
}
}
return 0;
}
8、复杂度分析
时间复杂度
两个dfs,O(n),太小,不考虑
总共m个询问,每个询问中,跑到lca需要跳链,平均每次跳
l
o
g
2
n
log_2n
log2n跳链,同时线段树维护O(logn)
所以总复杂度
O
(
m
l
o
g
2
2
n
)
O(mlog_2^2n)
O(mlog22n)
空间复杂度
空间主要是线段树和链式前向星的空间,不多
9、心得
维护边权题型
分析树剖性质,发现线段树维护的是点权,那么万一有题目需要维护边权呢
采用点代边的方法
用一条边的两端点中深度更大的那个点代表这条边(感性理解一下,没毛病)
线段树骚操作
树剖其实都很裸。。。敲多了发现真的非常简单。。
主要是线段树怎么维护,怎么pushup,怎么pushdown,这还是要有点技术在的
10、练习题
我貌似把我做的所有树剖题都写了题解,都是裸题。。
维护边权题/qtree系列:Qtree1
不是特别裸,要思考一下:Qtree3
ZJOI2008真题,不过真的裸:树的统计
小思考,NOI题:软件包管理器
pushdown见真章,细节注意:月下“毛景树”
弱省省选,裸题:[HEOI2016/TJOI2016]树
弱省省选,裸题:[SHOI2012]魔法树
弱省省选,裸题:[HAOI2015]树上操作
维护边权异或和:让我们异或吧
动态开点线段树+树剖:[SDOI2014]旅行
11、后记
树剖到这讲完了吧,水平有限,文笔不佳,若是没懂可以去看看别的博,我把我目前对树剖的理解都写上了,撒花~~