树链剖分是一种树路径信息维护算法。
把整棵树划分成许多条链,使每个节点都在唯一的链上,对每一条链维护一棵线段树,把在树上的操作转移到线段树上。
将一棵树划分成若干条链,用数据结构去维护每条链,保证每个点在且仅在一条链上,通过数据结构维护这些链的信息,复杂度为$ O(logN) $
接下来我们以一道例题为例。
如题,已知一棵包含 $ N $ 个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:
- 操作1: 格式: $1\ x\ y\ z $ 表示将树从x到y结点最短路径上所有节点的值都加上 $ z $
- 操作2: 格式: $2\ x\ y $ 表示求树从x到y结点最短路径上所有节点的值之和
- 操作3: 格式: $3\ x\ z $表示将以x为根节点的子树内所有节点值都加上 $ z $
- 操作4: 格式: $4\ x\ $表示求以x为根节点的子树内所有节点值之和
有 $ M $ 次操作,其中 $N \leq 10^5,M \leq 10^5$
<h1>树链剖分</h1>
<h2>介绍</h2>
我们先明确一下概念:
- 重儿子:在一个点 $ u $ 的儿子中,拥有最大的 $size$ 值的点 ,就叫做是点 $ u $ 的重儿子。
- 轻儿子:点 $ u $ 的儿子中除了 $ u $ 的重儿子外的所有儿子。
- 重边:点 $ u $ 于其重儿子的连边。
- 轻边:点 $ u $ 与其轻儿子的连边。
- 重链:仅由重边构成的路径
那么我们不难发现,树链剖分有一些保证复杂度的优秀的性质。
- 性质1:如果 $ v $ 是 $ u $ 的轻儿子,那么 $ size_v \leq \frac{size_u}{2} $ 。
- 性质2:任意点 $ u $ 到根的路径上轻边,重链条数都不大于 $ log_2n $ 。
<h2>定义</h2>
树链剖分需要定义三种结构体,树的节点,树的边,链。
代码定义如下:
struct Node{
int dfn,size,depth; // dfn 表示 dfs 序,size 表示以该节点为根的子树的大小,depth 表示该节点的深度
bool vis; // 是否在第一次 dfs 中被访问过
Node fa,maxchild; // fa 表示该节点的父节点,maxchild 表示该节点的重儿子
struct Edge *firstEdge;
struct Chain *chain; // 该节点所在的链
}node[MAXN];
struct Edge{
Node s,t;
Edge *next;
}pool1[MAXN << 1],*frog1 = pool1;
Edge New1(Node s,Node *t){
Edge *ret = ++frog1;
ret->s = s;ret->t = t;
ret->next = s->firstEdge;
return ret;
}
inline void add(int u,int v){
node[u].firstEdge = New1(&node[u],&node[v]);
node[v].firstEdge = New1(&node[v],&node[u]);
}
struct Chain{
Node *top; // 表示链的顶部节点
// 这里还可以维护链的更多信息,例如长度
}pool2[MAXN],*frog2 = pool2;
Chain New2(Node top){
Chain *ret = ++frog2;
ret->top = top;
return ret;
}
<h2>剖分过程</h2>
这个过程是划分轻重链的过程。
我们用两遍 dfs 来实现。
对于第一遍 dfs,求出每个节点的 $ size,depth,fa,maxchild $ ,对于第二遍 dfs,求出 $ dfn,chain $ (定义见上文)
剖分过程代码如下:
void dfs1(Node *v){ // 第一遍 dfs
v->size = 1; // 初始化 size
v->vis = true; // 标记已被访问
for(Edge *e = v->firstEdge;e;e = e->next){
if(!e->t->vis){
e->t->fa = v;
e->t->depth = v->depth + 1;
dfs1(e->t);
v->size += e->t->size; // 累加 size
if(!v->maxchild || v->maxchild->size < e->t->size)
v->maxchild = e->t;
// 更新重儿子
}
}
}
void dfs2(Node *v){
static int ts = 0;
// 这里的 static 可以理解为全局变量,不会被重复定义
v->dfn = ++ts; // 获得 dfn 序列
if(!v->fa || v->fa->maxchild != v)
v->chain = New2(v);
else v->chain = v->fa->chain;
if(v->maxchild)
dfs2(v->maxchild); // 优先遍历重儿子
for(Edge *e = v->firstEdge;e;e = e->next){
if(e->t->fa == v && e->t != v->maxchild)
dfs2(e->t);
}
}
inline void split(int root){
node[root].depth = 1;
dfs1(&node[root]);
dfs2(&node[root]);
}
<h2>维护链上信息</h2>
我们使用线段树来维护信息,每个线段树节点的编号是树上节点所对应的 $ dfn $ 值。
线段树(或其他数据结构)的定义要随着题目的改变而改变。
SegmentTree *segt = SegmentTree::build(1,N)
<h2>查询&修改</h2>
对于树链剖分后的查询和修改操作,只需要考虑如何转换到线段树上的区间修改操作即可。
我们以操作 $ 1 $ 和 $ 2 $ 为例,设 $ u $ 的深度更深,我们先让 $ u $ 跳到 $ v $ 所在的链上,途中进行 修改/查询 ,最后对 $ u $ 和 $ v $ 剩下的区域进行 修改/查询 即可。
inline void modify1(int x,int y,LL delta){
Node u = &node[x],v = &node[y];
while(u->chain != v->chain){ // 调到同一个链上,
if(u->chain->top->depth < v->chain->top->depth) std::swap(u,v);
segt->modify(u->chain->top->dfn,u->dfn,delta);
u = u->chain->top->fa;
}
if(u->depth > v->depth) std::swap(u,v);
segt->modify(u->dfn,v->dfn,delta);
}
inline LL query1(int x,int y){
Node u = &node[x],v = &node[y];
LL ret = 0;
while(u->chain != v->chain){
if(u->chain->top->depth < v->chain->top->depth) std::swap(u,v);
ret = (ret + segt->query(u->chain->top->dfn,u->dfn)) % ha;
u = u->chain->top->fa;
}
if(u->depth > v->depth) std::swap(u,v);
ret = (ret + segt->query(u->dfn,v->dfn)) % ha;
return ret;
}
操作二留给读者作为思考题,要考虑修改的内容和 $ dfn $ 序的关系,进而才能确定与线段树修改的关系。
<h2>最近公共祖先</h2>
树剖求最近公共祖先,也是先让 $ u $ 跳到 $ v $ 所在的链上,然后返回 $ depth $ 小的点。
Node lca(Node u,Node *v){
while(u->chain != v->chain){
if(u->chain->top->depth < v->chain->top->depth) std::swap(u,v);
u = u->chain->top->fa;
}
if(u->depth > v->depth) std::swap(u,v);
return u;
}
<h2>全部代码</h2>
#include <algorithm>
#include <iostream>
#include <cstring>
#include <climits>
#include <cstdio>
#include <vector>
#include <cmath>
#include <queue>
#include <stack>
#include <map>
#include <set>
const int MAXN = 1000000 + 5;
#define LL long long
#define ULL unsigned long long
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl
int N,M,R,ha;
LL dist[MAXN];
struct Node{
int dfn,size,depth;
bool vis;
Node fa,maxchild;
struct Edge *firstEdge;
struct Chain *chain;
}node[MAXN];
struct Edge{
Node s,t;
Edge *next;
}pool1[MAXN << 1],*frog1 = pool1;
Edge New1(Node s,Node *t){
Edge *ret = ++frog1;
ret->s = s;ret->t = t;
ret->next = s->firstEdge;
return ret;
}
inline void add(int u,int v){
node[u].firstEdge = New1(&node[u],&node[v]);
node[v].firstEdge = New1(&node[v],&node[u]);
}
struct Chain{
Node *top;
}pool2[MAXN],*frog2 = pool2;
Chain New2(Node top){
Chain *ret = ++frog2;
ret->top = top;
return ret;
}
void dfs1(Node *v){
v->size = 1;
v->vis = true;
for(Edge *e = v->firstEdge;e;e = e->next){
if(!e->t->vis){
e->t->fa = v;
e->t->depth = v->depth + 1;
dfs1(e->t);
v->size += e->t->size;
if(!v->maxchild || v->maxchild->size < e->t->size)
v->maxchild = e->t;
}
}
}
void dfs2(Node *v){
static int ts = 0;
v->dfn = ++ts;
if(!v->fa || v->fa->maxchild != v)
v->chain = New2(v);
else v->chain = v->fa->chain;
if(v->maxchild)
dfs2(v->maxchild);
for(Edge *e = v->firstEdge;e;e = e->next){
if(e->t->fa == v && e->t != v->maxchild)
dfs2(e->t);
}
}
inline void split(int root){
node[root].depth = 1;
dfs1(&node[root]);
dfs2(&node[root]);
}
struct SegmentTree;
SegmentTree New3(int ,int ,SegmentTree ,SegmentTree *);
struct SegmentTree {
int l,r;
LL sum,tag;
SegmentTree lc,rc;
static SegmentTree *build(int l,int r){
int mid = (l + r) >> 1;
return (l == r) ? New3(l,r,NULL,NULL) : New3(l,r,build(l,mid),build(mid + 1,r));
}
inline void pushup(){
sum = (lc->sum + rc->sum) % ha;
}
inline void cover(LL delta){
sum = (sum + ((r - l + 1) * delta) % ha) % ha;
tag = (tag + delta) % ha;
}
inline void pushdown(){
if(tag){
lc->cover(tag);
rc->cover(tag);
tag = 0;
}
}
void update(int pos,LL x){
if(l == r){
sum = x;
return;
}
LL mid = (l + r) >> 1;
if(pos <= mid) lc->update(pos,x);
else rc->update(pos,x);
pushup();
}
void modify(int left,int right,LL delta){
if(left == l && right == r){
cover(delta);
return;
}
if(left > r || right < l) return;
pushdown();
int mid = (l + r) >> 1;
if(right <= mid) lc->modify(left,right,delta);
else if(left > mid) rc->modify(left,right,delta);
else{
lc->modify(left,mid,delta);
rc->modify(mid + 1,right,delta);
}
pushup();
}
LL query(int left,int right){
if(left == l && right == r) return sum%ha;
if(left > r || right < l) return 0;
pushdown();
int mid = (l + r) >> 1;
if(right <= mid) return lc->query(left,right);
else if(left > mid) return rc->query(left,right);
return (lc->query(left,mid) + rc->query(mid + 1,right))%ha;
}
}pool3[MAXN << 2],frog3 = pool3,segt;
SegmentTree New3(int l,int r,SegmentTree lc,SegmentTree *rc){
SegmentTree *ret = ++frog3;
ret->l = l;ret->r = r;
ret->lc = lc;ret->rc = rc;
ret->sum = ret->tag = 0;
return ret;
}
inline void update(int x,LL delta){
Node *v = &node[x];
segt->update(v->dfn,delta);
}
inline void modify1(int x,int y,LL delta){
Node u = &node[x],v = &node[y];
while(u->chain != v->chain){
if(u->chain->top->depth < v->chain->top->depth) std::swap(u,v);
segt->modify(u->chain->top->dfn,u->dfn,delta);
u = u->chain->top->fa;
}
if(u->depth > v->depth) std::swap(u,v);
segt->modify(u->dfn,v->dfn,delta);
}
inline LL query1(int x,int y){
Node u = &node[x],v = &node[y];
LL ret = 0;
while(u->chain != v->chain){
if(u->chain->top->depth < v->chain->top->depth) std::swap(u,v);
ret = (ret + segt->query(u->chain->top->dfn,u->dfn)) % ha;
u = u->chain->top->fa;
}
if(u->depth > v->depth) std::swap(u,v);
ret = (ret + segt->query(u->dfn,v->dfn)) % ha;
return ret;
}
inline void modify2(int x,LL delta){
Node *v = &node[x];
segt->modify(v->dfn,v->dfn + v->size - 1,delta);
}
inline LL query2(int x){
Node *v = &node[x];
return segt->query(v->dfn,v->dfn + v->size - 1) % ha;
}
int main(){
scanf("%d%d%d%d",&N,&M,&R,&ha);
segt = SegmentTree::build(1,N);
for(int i = 1;i <= N;i++)
scanf("%lld",dist + i);
for(int u,v,i = 1;i < N;i++){
scanf("%d%d",&u,&v);
add(u,v);
}
split(R);
for(int i = 1;i <= N;i++)
update(i,dist[i]);
int opt,x,y;LL delta;
while(M--){
scanf("%d",&opt);
switch(opt){
case 1:
scanf("%d%d%lld",&x,&y,&delta);
modify1(x,y,delta);
break;
case 2:
scanf("%d%d",&x,&y);
printf("%lldn",query1(x,y)%ha);
break;
case 3:
scanf("%d%lld",&x,&delta);
modify2(x,delta);
break;
case 4:
scanf("%d",&x);
printf("%lldn",query2(x)%ha);
break;
}
}
return 0;
}
思考题答案:我们考虑修改一个以 $ v $ 为根子树的时候,最小的 $ dfn $ 的节点是 $ v $,$ dfn $ 最大值就是 $ dfn_v + size_v - 1 $。
One comment
Orz wyh