學習筆記——樹鏈剖分
思想
對於每個節點,把所有子節點中子樹最大的一個,成為重點,其它成為輕點。重點到父親節點的連線成為重邊,重邊連線成若干條重鏈,其餘的每個點稱為重鏈。
可以發現,如果路徑經過一條輕邊,那麼現在的子樹大小至少縮小一半,所以每條路徑可以被拆分成最多 \(\log n\) 條鏈,這樣一來,就可以把較難維護的樹形結構,改變成若干條鏈,並且可以發現按照先重後輕的原則遍歷時,屬於相同重鏈或相同子樹的遍歷序是連續的,就可以根據這個性質用線段樹維護。
每個鏈線上段樹上的區間是 \([dfn(top(u)),dfn(u)]\),每個子樹線上段樹上的區間是 \([dfn(u),dfn(u)+siz(u)-1]\),根據這個進行線段樹操作。
模板
Code
#include<iostream> #include<algorithm> #include<cmath> #include<cstdio> #include<cstring> #include<queue> #include<stack> #include<vector> #include<map> using namespace std; #define re register typedef int ll; typedef double db; typedef unsigned long long ull; const int maxn=1e6+10; const int maxm=1e6+10; const ll mod=1e9+7; const ull base1=19260817; const ull base2=19660813; const ll maxxn=0x7ffffff; const ll minxn=-0x7ffffff; const db inf=1e13; #define mid ((l+r)>>1) #define lson rt<<1,l,mid #define rson rt<<1|1,mid+1,r #define len (r-l+1) inline ll read(){ ll x=0,w=1;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();} while(c<='9'&&c>='0'){x=(x<<3)+(x<<1)+c-'0';c=getchar();} return x*w; } inline void write(ll x){ if(x<0){putchar('-');write(-x);return;} if(x>=10) write(x/10); putchar(x%10+'0'); } ll n,m,root,p; struct edge{ ll to,nxt; }e[maxn]; ll cnt,head[maxn]; ll w[maxn],dfnw[maxn]; ll fa[maxn],son[maxn],dep[maxn],siz[maxn]; ll dfn[maxn],top[maxn],dfncnt; inline void add_edge(ll u,ll v){ e[++cnt].to=v; e[cnt].nxt=head[u]; head[u]=cnt; } //第一次dfs遍歷深度,父親,子樹大小和重兒子 inline void dfs1(ll u,ll f,ll d){ dep[u]=d,fa[u]=f,siz[u]=1; ll maxson=-1; for(int i=head[u];i;i=e[i].nxt){ ll v=e[i].to; if(v==f) continue; dfs1(v,u,d+1); siz[u]+=siz[v]; if(siz[v]>maxson){ son[u]=v; maxson=siz[v]; } } } //第二次dfs遍歷重鏈的頂點和dfs序 inline void dfs2(ll u,ll t){ dfn[u]=++dfncnt,dfnw[dfncnt]=w[u],top[u]=t; if(!son[u]) return; dfs2(son[u],t);//先遍歷重兒子保證dfs序連續 for(int i=head[u];i;i=e[i].nxt){ ll v=e[i].to; if(v==fa[u]||v==son[u]) continue; dfs2(v,v);//輕兒子是所在重鏈的頂點 } } ll num[maxn],laz[maxn]; //線段樹維護 struct XDS{ inline void build(ll rt,ll l,ll r){ if(l==r){ num[rt]=dfnw[l]%p; return; } build(lson),build(rson); num[rt]=(num[rt<<1]+num[rt<<1|1])%p; } inline void push_down(ll rt,ll l,ll r){ laz[rt<<1]+=laz[rt],laz[rt<<1|1]+=laz[rt]; num[rt<<1]+=laz[rt]*(mid-l+1),num[rt<<1|1]+=laz[rt]*(r-mid); num[rt<<1]%=p,num[rt<<1|1]%=p,laz[rt]=0; } inline void update(ll rt,ll l,ll r,ll pl,ll pr,ll k){ if(pl<=l&&r<=pr){ laz[rt]+=k; num[rt]+=k*len; } else{ if(laz[rt]) push_down(rt,l,r); if(pl<=mid) update(lson,pl,pr,k); if(pr>mid) update(rson,pl,pr,k); num[rt]=(num[rt<<1]+num[rt<<1|1])%p; } } inline ll query(ll rt,ll l,ll r,ll pl,ll pr){ if(pl<=l&&r<=pr){ return num[rt]%p; } else{ if(laz[rt]) push_down(rt,l,r); ll sum=0; if(pl<=mid) sum+=query(lson,pl,pr); if(pr>mid) sum+=query(rson,pl,pr); return sum; } } }tree; //處理區間時,因為一定在一條重鏈向下延伸的重鏈中,所以一直向上跳深度較大的一點直到lca inline void update_pt(ll x,ll y,ll k){ k%=p; while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]]) swap(x,y); tree.update(1,1,n,dfn[top[x]],dfn[x],k); x=fa[top[x]]; } if(dep[x]>dep[y]) swap(x,y); tree.update(1,1,n,dfn[x],dfn[y],k); } inline ll query_pt(ll x,ll y){ ll sum=0; while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]]) swap(x,y); sum=(sum+tree.query(1,1,n,dfn[top[x]],dfn[x]))%p; x=fa[top[x]]; } if(dep[x]>dep[y]) swap(x,y); sum=(sum+tree.query(1,1,n,dfn[x],dfn[y]))%p; return sum%p; } //處理子樹時,因為dfs序一定是連續的,所以與線段樹區間處理相同 inline void update_tr(ll x,ll k){ tree.update(1,1,n,dfn[x],dfn[x]+siz[x]-1,k); } inline ll query_tr(ll x){ return tree.query(1,1,n,dfn[x],dfn[x]+siz[x]-1)%p; } int main(){ n=read(),m=read(),root=read(),p=read(); for(int i=1;i<=n;i++){ w[i]=read(); } for(int i=1;i<n;i++){ ll u=read(),v=read(); add_edge(u,v),add_edge(v,u); } dfs1(root,0,1); dfs2(root,root); tree.build(1,1,n); while(m--){ ll opt=read(); if(opt==1){ ll x=read(),y=read(),k=read(); update_pt(x,y,k); } else if(opt==2){ ll x=read(),y=read(); printf("%d\n",query_pt(x,y)%p); } else if(opt==3){ ll x=read(),k=read(); update_tr(x,k); } else{ ll x=read(); printf("%d\n",query_tr(x)%p); } } }
例題
1.P2690 ZJOI2008 書的統計
甚至比模板還要簡單,沒有複雜的轉換,只需要維護區間和以及區間最大值,直接樹剖套線段樹。
程式碼鴿了。
2.P2486 SDOI2011 染色
一道很有趣的題。
因為要維護連續顏色段數,線段樹的查詢和修改就略有不同,函式引數應為:當前下標,當前區間,當前查詢區間,要求查詢區間,這樣能夠維護出邊界的顏色,來特判左右兩段是否合併為一個。
ACcode
#include<iostream> #include<algorithm> #include<cmath> #include<cstdio> #include<cstring> #include<queue> #include<stack> #include<vector> #include<map> using namespace std; #define re register typedef int ll; typedef double db; typedef unsigned long long ull; const int maxn=1e6+10; const int maxm=1e6+10; const ll mod=1e9+7; const ull base1=19260817; const ull base2=19660813; const ll maxxn=0x7ffffff; const ll minxn=-0x7ffffff; const db inf=1e13; #define mid ((l+r)>>1) #define lson rt<<1,l,mid #define rson rt<<1|1,mid+1,r #define len (r-l+1) inline ll read(){ ll x=0,w=1;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();} while(c<='9'&&c>='0'){x=(x<<3)+(x<<1)+c-'0';c=getchar();} return x*w; } inline void write(ll x){ if(x<0){putchar('-');write(-x);return;} if(x>=10) write(x/10); putchar(x%10+'0'); } ll n,m; struct edge{ ll to,nxt; }e[maxn]; ll cnt,head[maxn]; inline void add_edge(ll u,ll v){ e[++cnt].to=v; e[cnt].nxt=head[u]; head[u]=cnt; } ll w[maxn]; ll fa[maxn],son[maxn],dep[maxn],siz[maxn]; ll dfn[maxn],top[maxn],dfncnt; inline void dfs1(ll u,ll f,ll d){ dep[u]=d,fa[u]=f,siz[u]=1; ll maxson=-1; for(int i=head[u];i;i=e[i].nxt){ ll v=e[i].to; if(v==f) continue; dfs1(v,u,d+1); siz[u]+=siz[v]; if(siz[v]>maxson){ son[u]=v; maxson=siz[v]; } } } inline void dfs2(ll u,ll t){ dfn[u]=++dfncnt,top[u]=t; if(!son[u]) return; dfs2(son[u],t); for(int i=head[u];i;i=e[i].nxt){ ll v=e[i].to; if(v==fa[u]||v==son[u]) continue; dfs2(v,v); } } ll num[maxn],laz[maxn],lc[maxn],rc[maxn]; ll cl,cr; struct XDS{ inline void push_down(ll rt){ if(laz[rt]){ laz[rt<<1]=laz[rt<<1|1]=laz[rt]; num[rt<<1]=num[rt<<1|1]=1; lc[rt<<1]=rc[rt<<1]=lc[rt]; lc[rt<<1|1]=rc[rt<<1|1]=lc[rt]; laz[rt]=0; } } inline void push_up(ll rt){ lc[rt]=lc[rt<<1],rc[rt]=rc[rt<<1|1]; ll ans=num[rt<<1]+num[rt<<1|1]; if(rc[rt<<1]==lc[rt<<1|1]) ans--; num[rt]=ans; } inline void build(ll rt,ll l,ll r){ num[rt]=0; if(l==r){ return; } build(lson),build(rson); } inline void update(ll rt,ll l,ll r,ll nl,ll nr,ll k){ if(nl==l&&r==nr){ num[rt]=laz[rt]=1; lc[rt]=rc[rt]=k; return; } push_down(rt); if(nr<=mid) update(lson,nl,nr,k); else if(nl>mid) update(rson,nl,nr,k); else{ update(lson,nl,mid,k),update(rson,mid+1,nr,k); } push_up(rt); } inline ll query(ll rt,ll l,ll r,ll nl,ll nr,ll pl,ll pr){ if(l==pl) cl=lc[rt]; if(r==pr) cr=rc[rt]; if(nl==l&&r==nr){ return num[rt]; } push_down(rt); if(pr<=mid) return query(lson,nl,nr,pl,pr); else if(pl>mid) return query(rson,nl,nr,pl,pr); else{ ll ans=query(lson,nl,mid,pl,pr)+query(rson,mid+1,nr,pl,pr); if(rc[rt<<1]==lc[rt<<1|1]) ans--; return ans; } push_up(rt); } }tree; inline void update_pt(ll u,ll v,ll k){ while(top[u]!=top[v]){ if(dep[top[u]]<dep[top[v]]) swap(u,v); tree.update(1,1,n,dfn[top[u]],dfn[u],k); u=fa[top[u]]; } if(dep[u]>dep[v]) swap(u,v); tree.update(1,1,n,dfn[u],dfn[v],k); } inline ll query_pt(ll u,ll v){ ll ansl=-1,ansr=-1; ll ans=0; while(top[u]!=top[v]){ if(dep[top[u]]<dep[top[v]]) swap(u,v),swap(ansl,ansr); ans+=tree.query(1,1,n,dfn[top[u]],dfn[u],dfn[top[u]],dfn[u]); if(cr==ansl) ans--; ansl=cl; u=fa[top[u]]; } if(dep[u]<dep[v]) swap(u,v),swap(ansl,ansr); ans+=tree.query(1,1,n,dfn[v],dfn[u],dfn[v],dfn[u]); if(cr==ansl) ans--; if(cl==ansr) ans--; return ans; } int main(){ n=read(),m=read(); for(int i=1;i<=n;i++){ w[i]=read(); } for(int i=1;i<n;i++){ ll u=read(),v=read(); add_edge(u,v),add_edge(v,u); } dfs1(1,0,1); dfs2(1,1); tree.build(1,1,n); for(int i=1;i<=n;i++){ tree.update(1,1,n,dfn[i],dfn[i],w[i]); } while(m--){ char opt; cin>>opt; if(opt=='C'){ ll u=read(),v=read(),k=read(); update_pt(u,v,k); } else{ ll u=read(),v=read(); printf("%d\n",query_pt(u,v)); } } }
3.P2590 ZJOI2008 樹的統計
模板題,程式碼不貼了。
4.P1967 貨車運輸
\(\operatorname{Kruskal}\)最大生成樹重構一下,可以放到樹剖裡跑,因為維護的是樹上邊權最小值,可以把邊權掛在子節點上,處理在同一重鏈的 \([L,R]\) 的路徑時,線段樹查詢 \([L+1,R]\) 的最小值即可。
兩個\(\operatorname{dfs}\)
ll fa[maxn],son[maxn],dep[maxn],siz[maxn];
ll dfn[maxn],top[maxn],tow[maxn],dfnw[maxn],dfncnt;
inline void dfs1(ll u,ll f,ll d){
dep[u]=d,fa[u]=f,siz[u]=1;
ll maxson=-1;
for(int i=head[u];i;i=e[i].nxt){
ll v=e[i].to;
if(v==f) continue;
tow[v]=e[i].w;
dfs1(v,u,d+1);
siz[u]+=siz[v];
if(siz[v]>maxson){
son[u]=v;
maxson=siz[v];
}
}
}
inline void dfs2(ll u,ll t){
dfn[u]=++dfncnt,dfnw[dfncnt]=tow[u],top[u]=t;
if(!son[u]) return;
dfs2(son[u],t);
for(int i=head[u];i;i=e[i].nxt){
ll v=e[i].to;
if(v==fa[u]||v==son[u]) continue;
dfs2(v,v);
}
}
查詢操作
inline ll query_ptmin(ll u,ll v){
ll ans=maxxn;
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]]) swap(u,v);
ans=min(ans,tree.query_min(1,1,n,dfn[top[u]],dfn[u]));
u=fa[top[u]];
}
if(dep[u]<dep[v]) swap(u,v);
ans=min(ans,tree.query_min(1,1,n,dfn[v]+1,dfn[u]));
return ans;
}
5.P3979 遙遠的國度
換根的樹剖,支援區間修改和區間維護最小值。
對於換根,因為樹的性質不變,所以如果在初始化以 \(1\) 為根的樹中,現在根節點和查詢節點同屬一顆子樹的同左或同右的子樹,就直接查詢這個子樹,如果在異側那就找到子樹的根,然後查詢兩次。
ACcode
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
#include<vector>
#include<map>
using namespace std;
#define re register
typedef long long ll;
typedef double db;
typedef unsigned long long ull;
const int maxn=1e5+10;
const int maxm=4e5+10;
const ll mod=1e9+7;
const ull base1=19260817;
const ull base2=19660813;
const ll maxxn=0x7fffffff;
const ll minxn=-0x7fffffff;
const db inf=1e13;
#define mid ((l+r)>>1)
#define lson rt<<1,l,mid
#define rson rt<<1|1,mid+1,r
#define len (r-l+1)
inline ll read(){
ll x=0,w=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
while(c<='9'&&c>='0'){x=(x<<3)+(x<<1)+c-'0';c=getchar();}
return x*w;
}
inline void write(ll x){
if(x<0){putchar('-');write(-x);return;}
if(x>=10) write(x/10);
putchar(x%10+'0');
}
ll n,m,root;
struct edge{
ll to,nxt;
}e[maxm];
ll head[maxn],cnt;
inline void add_edge(ll u,ll v){
e[++cnt].to=v;
e[cnt].nxt=head[u];
head[u]=cnt;
}
ll w[maxn];
ll fa[maxn],son[maxn],dep[maxn],siz[maxn];
ll dfn[maxn],top[maxn],dfncnt,dfnw[maxn];
inline void dfs1(ll u,ll f,ll d){
fa[u]=f,dep[u]=d,siz[u]=1;
ll maxson=-1;
for(int i=head[u];i;i=e[i].nxt){
ll v=e[i].to;
if(v==f) continue;
dfs1(v,u,d+1);
siz[u]+=siz[v];
if(siz[v]>maxson){
maxson=siz[v];
son[u]=v;
}
}
}
inline void dfs2(ll u,ll t){
dfn[u]=++dfncnt,dfnw[dfncnt]=w[u],top[u]=t;
if(!son[u]) return;
dfs2(son[u],t);
for(int i=head[u];i;i=e[i].nxt){
ll v=e[i].to;
if(v==fa[u]||v==son[u]) continue;
dfs2(v,v);
}
}
ll minx[maxm],laz[maxm];
struct XDS{
inline void push_up(ll rt){
minx[rt]=min(minx[rt<<1],minx[rt<<1|1]);
}
inline void build(ll rt,ll l,ll r){
if(l==r){
minx[rt]=dfnw[l];
return;
}
build(lson),build(rson);
push_up(rt);
}
inline void push_down(ll rt){
if(laz[rt]){
laz[rt<<1]=laz[rt<<1|1]=laz[rt];
minx[rt<<1]=minx[rt<<1|1]=laz[rt];
laz[rt]=0;
}
}
inline void updatex(ll rt,ll l,ll r,ll ql,ll qr,ll k){
if(ql<=l&&r<=qr){
minx[rt]=laz[rt]=k;
return;
}
push_down(rt);
if(ql<=mid) updatex(lson,ql,qr,k);
if(qr>mid) updatex(rson,ql,qr,k);
push_up(rt);
}
inline ll queryx(ll rt,ll l,ll r,ll ql,ll qr){
if(ql<=l&&r<=qr){
return minx[rt];
}
ll ans=maxxn;
push_down(rt);
if(ql<=mid) ans=min(ans,queryx(lson,ql,qr));
if(qr>mid) ans=min(ans,queryx(rson,ql,qr));
push_up(rt);
return ans;
}
}tree;
inline void update_pt(ll u,ll v,ll k){
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]]) swap(u,v);
tree.updatex(1,1,n,dfn[top[u]],dfn[u],k);
u=fa[top[u]];
}
if(dep[u]>dep[v]) swap(u,v);
tree.updatex(1,1,n,dfn[u],dfn[v],k);
}
inline ll find(ll u){
if(u==root) return -1;
if(dfn[u]>=dfn[root]||dfn[u]+siz[u]-1<dfn[root]) return 0;
ll tmp=root;
while(top[tmp]!=top[u]){
if(fa[top[tmp]]==u) return top[tmp];
tmp=fa[top[tmp]];
}
return son[u];
}
inline ll query_tr(ll u){
ll ty=find(u);
if(ty==-1) return minx[1];
if(!ty) return tree.queryx(1,1,n,dfn[u],dfn[u]+siz[u]-1);
else{
ll ans=tree.queryx(1,1,n,1,dfn[ty]-1);
if(dfn[ty]+siz[ty]-1!=n){
ans=min(ans,tree.queryx(1,1,n,dfn[ty]+siz[ty],n));
}
return ans;
}
}
int main(){
n=read(),m=read();
for(int i=1;i<n;i++){
ll u=read(),v=read();
add_edge(u,v),add_edge(v,u);
}
for(int i=1;i<=n;i++){
w[i]=read();
}
dfs1(1,0,1);
dfs2(1,1);
tree.build(1,1,n);
root=read();
for(int i=1;i<=m;i++){
ll opt=read();
if(opt==1) root=read();
else if(opt==2){
ll u=read(),v=read(),k=read();
update_pt(u,v,k);
}
else{
ll u=read();
printf("%lld\n",query_tr(u));
}
}
return 0;
}
5.P1505 國家集訓隊 旅遊
多用懶標記維護幾個東西就ok了,因為要維護最大值以及最小值,所以取負的時候直接交換最大最小值。
放個上傳和下傳程式碼
inline void push_up(ll rt){
maxx[rt]=max(maxx[rt<<1],maxx[rt<<1|1]);
minx[rt]=min(minx[rt<<1],minx[rt<<1|1]);
sum[rt]=sum[rt<<1]+sum[rt<<1|1];
}
inline void push_down(ll rt){
laz[rt<<1]^=1,laz[rt<<1|1]^=1;
maxx[rt<<1]=-maxx[rt<<1],maxx[rt<<1|1]=-maxx[rt<<1|1];
minx[rt<<1]=-minx[rt<<1],minx[rt<<1|1]=-minx[rt<<1|1];
sum[rt<<1]=-sum[rt<<1],sum[rt<<1|1]=-sum[rt<<1|1];
swap(maxx[rt<<1],minx[rt<<1]);
swap(maxx[rt<<1|1],minx[rt<<1|1]);
laz[rt]=0;
}
6.P3258 JLOI2014 松鼠的新家
區間修改+單點查詢
注意每次走的終點要減\(1\),因為會重複。
7.P2146 NOI2015 軟體包管理器
- 安裝操作:把根節點到當前節點路徑賦值為\(1\)
- 解除安裝操作:當前節點子樹賦值為\(0\)
輸出變化量。