Step 0-介紹
在之前線段樹的學習中,我們知道了如何對一個區間進行快速修改。
同樣我們可以在樹上進行快速修改(什么腦回路),完成以下幾個操作:
-
修改樹上兩點之間的路徑上所有點的值。
-
查詢樹上兩點之間的路徑上節點權值的和/極值/其它(在序列上可以用數據結構維護,便於合並的信息)。
-
此外,樹剖還可以用來 \(O(\log n)\)(且常數較小)地求 \(LCA\)。在某些題目中,還可以利用其性質來靈活地運用樹剖。
Q:那么在一棵樹上要如何進行修改呢?
沒錯!就是樹鏈剖分
Q:要怎么做呢?
就是把一顆樹 分屍(大霧 分成幾條鏈,再拼成一個線段樹...
那么接下來要解決的就是如何不重不漏的分屍了。
Step 1-幾種分屍方法
- 重鏈剖分
- 長鏈剖分(
不會) - 用於 Link/cut Tree 的剖分(有時被稱作“實鏈剖分”)(
不會)
ps:大多數情況下(沒有特別說明時), “樹鏈剖分” 都指 “重鏈剖分” 。
Step 2-詳解
1.重鏈剖分
(1)一些定義:
定義 重子節點 表示其子節點中子樹最大的子結點。如果有多個子樹最大的子結點,取其一。如果沒有子節點,就無重子節點。
定義 輕子節點 表示剩余的所有子結點。
從這個結點到重子節點的邊為 重邊 。
到其他輕子節點的邊為 輕邊 。
若干條首尾銜接的重邊構成 重鏈 。
把落單的結點也當作重鏈,那么整棵樹就被剖分成若干條重鏈。
如圖:
(2)重鏈剖分的性質
-
樹上每個節點都屬於且僅屬於一條重鏈 。
-
重鏈開頭的結點不一定是重子節點。
-
所有的重鏈將整棵樹 完全剖分 。
-
在剖分時 優先遍歷重兒子 ,最后重鏈的 DFS 序就會是連續的。
-
在剖分時 重邊優先遍歷 ,最后樹的 DFN 序上,重鏈內的 DFN 序是連續的。按 DFN 排序后的序列即為剖分后的鏈。
-
一顆子樹內的 DFN 序是連續的。
-
可以發現,當我們向下經過一條 輕邊 時,所在子樹的大小至少會除以二。
-
對於樹上的任意一條路徑,把它拆分成從 \(lca\) 分別向兩邊往下走,分別最多走 \(O(\log n)\) 次,因此,樹上的每條路徑都可以被拆分成不超過 \(O(\log n)\) 條重鏈。
(3)實現思路
樹剖的實現分兩個 DFS 的過程。偽代碼如下:
第一個 DFS 記錄每個結點的父節點(father)、深度(deep)、子樹大小(size)、重子節點(hson)。
第二個 DFS 記錄所在鏈的鏈頂(top,應初始化為結點本身)、重邊優先遍歷時的 DFS 序(dfn)、DFS 序對應的節點編號(rank)。
(4)代碼實現
定義幾個數組:
- \(fa(x)\) 表示節點 \(x\) 在樹上的父親。
- \(dep(x)\) 表示節點 \(x\) 在樹上的深度。
- \(siz(x)\) 表示節點 \(x\) 的子樹的節點個數。
- \(son(x)\) 表示節點 \(x\) 的 重兒子 。
- \(top(x)\) 表示節點 \(x\) 所在 重鏈 的頂部節點(深度最小)。
- \(dfn(x)\) 表示節點 \(x\) 的 DFS 序 ,也是其在線段樹中的編號。
- \(rnk(x)\) 表示 DFS 序所對應的節點編號,有 \(rnk(dfn(x))=x\) 。
我們進行兩遍 DFS 預處理出這些值,其中第一次 DFS 求出 \(fa(x)\) , \(dep(x)\) , \(siz(x)\) , \(son(x)\) ,第二次 DFS 求出 \(top(x)\) , \(dfn(x)\) , \(rnk(x)\) 。
void dfs1(int u) //第一次 DFS 求出 fa(x), dep(x), siz(x), son(x);
{
siz[u]=1;
for(int i=head[u];i;i=next[i])
{
int v=to[i];
if(v==fa[u]) continue;
dep[v]=dep[u]+1;
fa[v]=u;
dfs1(v);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
void dfs2(int u, int tt) //第二次 DFS 求出 top(x), dfn(x), rank(x);
{
top[u]=tt;
dfn[u]=++cnt;
rank[cnt]=u;
if(!son[u]) return;
dfs2(son[u],tt); //優先對重兒子進行 DFS,可以保證同一條重鏈上的點 DFS 序連續
for(int i=head[u];i;i=next[i])
{
int v=to[i];
if(v!=son[u]&&v!=fa[u])
dfs2(v,v);
}
}
Q:具體操作呢?
請進-->新人食用之樹剖
2.長鏈剖分
請進-->樹鏈剖分
3.用於 Link/cut Tree 的剖分
Step 3-常見應用
1.路徑上維護
用樹鏈剖分求樹上兩點路徑權值和,偽代碼如下:
鏈上的 DFS 序是連續的,可以使用線段樹、樹狀數組維護。
每次選擇深度較大的鏈往上跳,直到兩點在同一條鏈上。
同樣的跳鏈結構適用於維護、統計路徑上的其他信息。
2.子樹維護
有時會要求,維護子樹上的信息,譬如將以 \(x\) 為根的子樹的所有結點的權值增加 \(v\) 。
在 DFS 搜索的時候,子樹中的結點的 DFS 序是連續的。
每一個結點記錄 bottom 表示所在子樹連續區間末端的結點。
這樣就把子樹信息轉化為連續的一段區間信息。
3.求最近公共祖先
不斷向上跳重鏈,當跳到同一條重鏈上時,深度較小的結點即為 LCA。
向上跳重鏈時需要先跳所在重鏈頂端深度較大的那個。
參考代碼:
int lca(int u, int v) {
while (top[u] != top[v]) {
if (dep[top[u]] > dep[top[v]])
u = fa[top[u]];
else
v = fa[top[v]];
}
return dep[u] > dep[v] ? v : u;
}
Step 4-例題
1.P2590「ZJOI2008」樹的統計
My Code
//「ZJOI2008」樹的統計
#include <iostream>
#include <cstdio>
#define INF 10000005
using namespace std;
const int N=1e5+10;
struct TREE{
int l,r,v,maxx;
}t[N*4];
int n,q,a[N];
int head[N*2],to[N*2],next[N*2],tot;
int fa[N],siz[N],dfn[N],rank[N],son[N],top[N],dep[N],cnt;
void add(int u,int v)
{
next[++tot]=head[u];
head[u]=tot;
to[tot]=v;
}
void dfs1(int u)
{
siz[u]=1;
for(int i=head[u];i;i=next[i])
{
int v=to[i];
if(v==fa[u]) continue;
dep[v]=dep[u]+1;
fa[v]=u;
dfs1(v);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
void dfs2(int u,int tt)
{
top[u]=tt;
dfn[u]=++cnt;
rank[cnt]=u;
if(!son[u]) return ;
dfs2(son[u],tt);
for(int i=head[u];i;i=next[i])
{
int v=to[i];
if(v!=son[u]&&v!=fa[u])
dfs2(v,v);
}
}
void build(int k,int l,int r)
{
t[k].l=l,t[k].r=r;
if(l==r)
{
t[k].v=t[k].maxx=a[rank[l]];
return ;
}
int mid=(l+r)>>1;
build(k<<1,l,mid);
build(k<<1|1,mid+1,r);
t[k].v=t[k<<1].v+t[k<<1|1].v;
t[k].maxx=max(t[k<<1].maxx,t[k<<1|1].maxx);
}
void change(int k,int x,int w)
{
int l=t[k].l,r=t[k].r;
if(r<x||l>x) return ;
if(l==r&&l==x)
{
t[k].v=t[k].maxx=w;
return ;
}
int mid=(l+r)>>1;
if(x<=mid) change(k<<1,x,w);
else change(k<<1|1,x,w);
t[k].v=t[k<<1].v+t[k<<1|1].v;
t[k].maxx=max(t[k<<1].maxx,t[k<<1|1].maxx);
}
int qmax(int k,int x,int y)
{
int l=t[k].l,r=t[k].r;
if(l>=x&&r<=y) return t[k].maxx;
int mid=(l+r)>>1;
int maxx=-INF;
if(x<=mid) maxx=max(maxx,qmax(k<<1,x,y));
if(y>mid) maxx=max(maxx,qmax(k<<1|1,x,y));
return maxx;
}
int qsum(int k,int x,int y)
{
int l=t[k].l,r=t[k].r;
if(l>=x&&r<=y) return t[k].v;
int mid=(l+r)>>1;
int res=0;
if(x<=mid) res+=qsum(k<<1,x,y);
if(y>mid) res+=qsum(k<<1|1,x,y);
return res;
}
int QSUM(int x,int y)
{
int res=0;
while(top[x]!=top[y])
{
if(dep[top[x]]<dep[top[y]]) swap(x,y);
res+=qsum(1,dfn[top[x]],dfn[x]);
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
return res+=qsum(1,dfn[x],dfn[y]);
}
int QMAX(int x,int y)
{
int res=-INF;
while(top[x]!=top[y])
{
if(dep[top[x]]<dep[top[y]]) swap(x,y);
res=max(res,qmax(1,dfn[top[x]],dfn[x]));
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
return res=max(res,qmax(1,dfn[x],dfn[y]));
}
int main()
{
scanf("%d",&n);
int x,y;
for(int i=1;i<n;i++)
{
scanf("%d%d",&x,&y);
add(x,y),add(y,x);
}
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
dfs1(1);
dfs2(1,1);
build(1,1,n);
scanf("%d",&q);
char k[8];
while(q--)
{
cin>>k;
scanf("%d%d",&x,&y);
if(k[0]=='C') change(1,dfn[x],y);
else if(k[1]=='M') printf("%d\n",QMAX(x,y));
else printf("%d\n",QSUM(x,y));
}
return 0;
}
2.P3384 【模板】輕重鏈剖分
My Code
//P3384 【模板】輕重鏈剖分
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N=1e6+100;
struct TREE{
int v,l,r,add;
}t[N];
int n,m,p,root;
int cnt,tot,head[N],to[N],next[N];
int a[N];
int fa[N],dep[N],siz[N],son[N],top[N],dfn[N],rank[N];
inline int read()
{
char ch=getchar();int x=0,f=1;
while(ch<'0'||ch>'9')
{
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return x*f;
}
inline void add(int u,int v)
{
next[++tot]=head[u];
head[u]=tot;
to[tot]=v;
}
/*
我們先給出一些定義:
1.fa(x) 表示節點 x 在樹上的父親;
2.dep(x) 表示節點 x 在樹上的深度;
3.siz(x) 表示節點 x 的子樹的節點個數;
4.son(x) 表示節點 x 的重兒子 ;
5.top(x) 表示節點 x 所在重鏈的頂部節點(深度最小);
6.dfn(x) 表示節點 x 的 DFS 序 ,也是其在線段樹中的編號;
7.rank(x) 表示 DFS 序所對應的節點編號,有 rank(dfn(x))=x;
*/
void dfs1(int u) //第一次 DFS 求出 fa(x), dep(x), siz(x), son(x);
{
siz[u]=1;
for(int i=head[u];i;i=next[i])
{
int v=to[i];
if(v==fa[u]) continue;
dep[v]=dep[u]+1;
fa[v]=u;
dfs1(v);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
void dfs2(int u, int tt) //第二次 DFS 求出 top(x), dfn(x), rank(x);
{
top[u]=tt;
dfn[u]=++cnt;
rank[cnt]=u;
if(!son[u]) return;
dfs2(son[u],tt); //優先對重兒子進行 DFS,可以保證同一條重鏈上的點 DFS 序連續
for(int i=head[u];i;i=next[i])
{
int v=to[i];
if(v!=son[u]&&v!=fa[u])
dfs2(v,v);
}
}
/*
性質:
1.樹上每個節點都屬於且僅屬於一條重鏈 。
2.重鏈開頭的結點不一定是重子節點(因為重邊是對於每一個結點都有定義
的)。
3.所有的重鏈將整棵樹 完全剖分 。
4.在剖分時 優先遍歷重兒子 ,最后重鏈的 DFS 序就會是連續的。
5.在剖分時 重邊優先遍歷 ,最后樹的 DFN 序上,重鏈內的 DFN 序是連續
的。按 DFN 排序后的序列即為剖分后的鏈。
6.一顆子樹內的 DFN 序是連續的。
7.可以發現,當我們向下經過一條 輕邊 時,所在子樹的大小至少會除以二。
8.因此,對於樹上的任意一條路徑,把它拆分成從 lca 分別向兩邊往下走,
分別最多走 O(log n)次,因此,樹上的每條路徑都可以被拆分成不超過
O(log n)條重鏈。
*/
void build(int k,int l,int r)
{
t[k].l=l,t[k].r=r,t[k].add=0,t[k].v=0;
if(l==r)
{
t[k].v=a[rank[l]];
return ;
}
int mid=(l+r)>>1;
build(k<<1,l,mid);
build(k<<1|1,mid+1,r);
t[k].v=(t[k<<1].v+t[k<<1|1].v)%p;
}
void pushdown(int k)
{
if(t[k].add)
{
t[k<<1].add+=t[k].add;
t[k<<1|1].add+=t[k].add;
int l=t[k<<1].l,r=t[k<<1].r;
t[k<<1].v+=(r-l+1)*t[k].add;
t[k<<1].v%=p;
l=t[k<<1|1].l,r=t[k<<1|1].r;
t[k<<1|1].v+=(r-l+1)*t[k].add;
t[k<<1|1].v%=p;
t[k].add=0;
}
}
void qadd(int k,int x,int y,int v)
{
int l=t[k].l,r=t[k].r;
if(l>=x&&r<=y)
{
t[k].add+=v;
t[k].v+=(r-l+1)*v;
return ;
}
pushdown(k);
int mid=(l+r)>>1;
if(x<=mid) qadd(k<<1,x,y,v);
if(y>mid) qadd(k<<1|1,x,y,v);
t[k].v=(t[k<<1].v+t[k<<1|1].v)%p;
}
int query(int k,int x,int y)
{
int l=t[k].l,r=t[k].r;
if(l>=x&&r<=y) return t[k].v;
int mid=(l+r)>>1;
pushdown(k);
int res=0;
if(x<=mid) res+=query(k<<1,x,y);
if(y>mid) res+=query(k<<1|1,x,y);
return res;
}
void qsol(int x,int y,int v)
{
while(top[x]!=top[y])
{
if(dep[top[x]]<dep[top[y]]) swap(x,y);
qadd(1,dfn[top[x]],dfn[x],v);
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
qadd(1,dfn[x],dfn[y],v);
return ;
}
int query2(int x,int y)
{
int sum=0;
while(top[x]!=top[y])
{
if(dep[top[x]]<dep[top[y]]) swap(x,y);
sum+=query(1,dfn[top[x]],dfn[x]);
sum%=p;
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
sum+=query(1,dfn[x],dfn[y]);
sum%=p;
return sum;
}
int main()
{
int u,v,x,y,w,k;
n=read(),m=read(),root=read(),p=read();
for(int i=1;i<=n;i++) a[i]=read();
for(int i=1;i<n;i++)
{
u=read(),v=read();
add(u,v),add(v,u);
}
dfs1(root);
dfs2(root,root);
build(1,1,n);
while(m--)
{
k=read();
if(k==1)
{
x=read(),y=read(),w=read();
qsol(x,y,w%p);
}
if(k==2)
{
x=read(),y=read();
cout<<query2(x,y)<<endl;
}
if(k==3)
{
x=read(),w=read();
qadd(1,dfn[x],dfn[x]+siz[x]-1,w);
}
if(k==4)
{
x=read();
cout<<query(1,dfn[x],dfn[x]+siz[x]-1)%p<<endl;
}
}
return 0;
}
Step 5-寫在后面
參考資料
都看到這里了,不打算點個贊嘛