什么是树链剖分/重链剖分
我们可以弄一道例题来看看:
现在给定一棵 \(n(1 \le n \le 10^5)\) 节点的树,每个节点上有一个数值,现在你可以进行 m ( 1 \\le m \\le 10\^5) 次操作。格式如下:
1 x z
表示将 \(x\) 到 \(y\) 最短路径上的节点值加上 \(z\) 。2 x y
表示树求 \(x\) 到 \(y\) 最短路径上的权值和。3 x y
将子树 \(x\) 下的每个节点的权值加上 \(y\) 。4 x
求子树 \(x\) 下每个节点的权值之和。
怎么写呢?当然,你需要前置知识线段树。接下来,且听我慢慢道来。
先从构造一个完美的剖分数组开始
我们知道,一棵树可以通过DFS构造成一个序列,其中对于任意一棵子树(包括根节点),我们把他的节点个数叫做 \(siz\) 。子树 \(x\) 的大小就是 \(siz_x\) 。那这对剖分有什么帮助呢?对于一棵树的DFS序举例如下:
1
/ \
2 3
/
4
\
5
那么DFS后,序列如下:
1 2 4 5 3
可以看到,子树 \(x\) 到 \(x+siz_x\) ,就是整个子树 \(x\) 。我们暂时叫这个数组为剖分数组 \(tag\)。但这样并不是最优的,有的的时候无法保证前面阐述的规律,这个时候怎么办呢?我们可以分析一下,在这棵树中,\(1\) 有两个孩子,分别是 \(2\) 和 \(3\) ,而且 \(2\) 的子树大小比 \(3\) 大,那么我们就规定 \(2\) 是 \(1\) 的 重孩子 。从 \(1\) 到 \(2\) 的这条边我们叫做重链 。可以得知,对于任意一个非叶子节点,都有其独有的重孩子,我们就可以根据重孩子来构造DFS的先后,因为我们优先遍历一棵树的所有重孩子,所以所有重孩子一定是连在一起的,上面就是一个说明。那优先从重孩子开始还有什么好处呢?我们知道,如果从轻孩子开始遍历去构造这个数组,那么时间复杂度可能会达到 \(O(n)\)(见后文)。但是如果从重孩子开始的话,就不一样了,我们可以证明其时间复杂度可以来到 \(O(\log^2n)\) 的单次询问复杂度,证明如下:
对于节点 \(u\) ,其重孩子 \(v\) 必然是所有子节点中子树最大的那个,即为:
\[size(v) \ge size(w) \forall w \in children(u) \]
又因为重孩子的子树大小 \(\ge\) 其他所有子节点的子树大小,因此对于任意轻孩子 \(w\) 有:
\[size(w)\le size(v) \]
又又因为所有节点子树个数+1等于其父亲节点的包含的节点个数(有点拗口),即为:
\[size(v)+\sum_{轻孩子w} size(w)+1 = size(u) \]
根据上面的第二,三条定理,解不等式方程,可以得到对于:
\[\sum_{轻孩子w}size(w) \le size(u)-size(v)-1 \]
所以可以知道:**如果某个轻孩子节点\(w\) 的子树大小大于 \(\frac{1}{2} size(u)\), 则以上定理不成立,违背重孩子的定义,所以,必然有对于任意 \(w\):
\[size(w)\le \frac{size(u)}{2} \]
因此,从根节点到任意节点得到路径上,最多只有 \(O(\log n)\) 条轻边(因为每次碰到轻边子树大小随见到原来的 \frac{1}{2},所以至多 \(\log^2n\) 就会到达根节点)
而又因为重孩子是连续的一段区间,所以一次线段树修改就可以,再加上轻边修改是每次 \(O(\log n)\),最多 \(\log n\) 条,所以是 \(O(\log n)\) 时间复杂度,证毕。
至此,我们的树链剖分的关键部分就已经完成。
Code如下:
cpp
void dfs1(int root, int fa) {
fath[root] = fa;
siz[root] = 1;
dep[root] = dep[fa] + 1;
for(auto i : tree[root]) {
if(i == fa) continue;
dfs1(i, root);
siz[root] += siz[i];
if(siz[i] > siz[son[root]])
son[root] = i;
}
}
void dfs2(int root, int tp) {
top[root] = tp;
dfn[root] = ++cnt;
rev[cnt] = root;
if(!son[root]) return;
dfs2(son[root], tp);
for(auto i : tree[root]) {
if(i == fath[root] || i == son[root]) continue;
dfs2(i, i);
}
}
其中 siz
是表示大小,top
表示链头(见后文),dfn
表示对于 \(root\) 节点,在剖分数组里的位置。rev
是反的 dfn
,cnt
全局计数器,son
记录重孩子编号,dep
表示深度。
利用链表的思想,完成perfect的查询/修改
我们已经知道,怎么分割一个有效的剖分数组。就是不会用,怎么办呢?
还是拿出一棵树:
1
/ \
2 5
/ \ \
3 7 6
\ /
4 8
/ \
9 10
构造出来的序列如下(-
表示重链,
表示轻边)
1-2-3-4-9 10 7 5-6-8
现在我们将 \(10\) 到 \(8\) 之间的,过程如下:
- 先使所有重链的链头为深度最小的节点,比如 \(8,6,5\) 的链头都是 \(5\),\(9,4,3,2,1\) 的链头都是 \(1\)
- 令所有轻边的链头就是其本身
那么修改的时候,每次选择深度大的条跳跃,直到两者链头相同。那么此时只需要修改当前 \(\min{u,v}\) 到 \(\max{u,v}\) (询问/操作的两个端点)就可以了!。
到这里我们也可以知道了吧,按照轻边来的话时间复杂度是 \(O(n)\) 的哦。千万小心!
因为查询和修改代码雷同,就不详细撰述了。
Code For Luogu P3384
cpp
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 1e5+100;
int num[maxn],fath[maxn],top[maxn],son[maxn],dep[maxn],siz[maxn];
int lay[maxn<<2],w[maxn<<2],tag1[maxn],tag2[maxn],cnt,n,m,r,p;
vector<vector<int> > tree;
inline void dfs1(int root,int fa){
fath[root]=fa;
siz[root]=1;
dep[root]=dep[fa]+1;
for(auto i : tree[root]){
if(i == fa)continue;
dfs1(i,root);
siz[root]+=siz[i];
if(siz[i]>siz[son[root]])
son[root]=i;
}
}
inline void dfs2(int root,int Top){
tag1[root]=++cnt;
tag2[cnt]=root;
top[root]=Top;
if(son[root]){
dfs2(son[root],Top);
for(auto i : tree[root]){
if(i == son[root] || i==fath[root])continue;
dfs2(i,i);
}
}
}
void pushup(int root){
w[root]=(w[root<<1]+w[root<<1|1])%p;
}
void maketag(int root,int l,int r,int lz){
lay[root]=(lay[root]+lz)%p;
w[root]=(w[root]+(r-l+1)*lz)%p;
}
void pushdn(int root,int l,int r){
if(lay[root]==0 || l==r) return;
int mid=(l+r)>>1;
maketag(root<<1,l,mid,lay[root]);
maketag(root<<1|1,mid+1,r,lay[root]);
lay[root]=0;
}
void build(int root,int l,int r){
if(l==r){
w[root]=num[tag2[l]]%p;
return ;
}
int mid=(l+r)>>1;
build(root<<1,l,mid);
build(root<<1|1,mid+1,r);
pushup(root);
}
int query(int root,int l,int r,int L,int R){
if(R<l || L>r) return 0;
if(L<=l && r<=R) return w[root];
pushdn(root,l,r);
int mid=(l+r)>>1;
return (query(root<<1,l,mid,L,R)+query(root<<1|1,mid+1,r,L,R))%p;
}
void update(int root,int l,int r,int L,int R,int val){
if(R<l || L>r) return;
if(L<=l && r<=R){
maketag(root,l,r,val);
return;
}
pushdn(root,l,r);
int mid=(l+r)>>1;
update(root<<1,l,mid,L,R,val);
update(root<<1|1,mid+1,r,L,R,val);
pushup(root);
}
void udp(int x,int y,int val){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
update(1,1,n,tag1[top[x]],tag1[x],val);
x=fath[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
update(1,1,n,tag1[x],tag1[y],val);
}
int qry(int x,int y){
int ans=0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
ans=(ans+query(1,1,n,tag1[top[x]],tag1[x]))%p;
x=fath[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
ans=(ans+query(1,1,n,tag1[x],tag1[y]))%p;
return ans;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>r>>p;
tree.resize(n+1);
for(int i=1;i<=n;i++) cin>>num[i];
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
tree[x].push_back(y);
tree[y].push_back(x);
}
dfs1(r,0);
dfs2(r,r);
build(1,1,n);
for(int i=1;i<=m;i++){
int op,x,y,z;
cin>>op;
if(op==1){
cin>>x>>y>>z;
udp(x,y,z);
}
else if(op==2){
cin>>x>>y;
cout<<qry(x,y)<<endl;
}
else if(op==3){
cin>>x>>z;
update(1,1,n,tag1[x],tag1[x]+siz[x]-1,z);
}
else if(op==4){
cin>>x;
cout<<query(1,1,n,tag1[x],tag1[x]+siz[x]-1)<<endl;
}
}
return 0;
}
线段树部分请自学哦!