樹鏈剖分
樹鏈剖分
前置芝士
就像它的名字,樹鏈剖分是在一棵樹上進行,在講解中還會用到線段樹和dfs,如果不會,開啟連結自行搜尋(主要是線段樹的部落格沒做,還有不要問我為什麼這算知識)。
一個節點的重兒子,為其更大的一顆子樹的根節點。從這個點連向重兒子的邊我們稱為重邊。
由重邊連續連起來的點和邊就組成了重鏈,也就是樹鏈。
概念
將一棵樹劃分成若干條鏈,用資料結構去維護每條鏈,複雜度為 \(O(logN)\) 。
其實本質是一些資料結構/演算法在樹上的推廣
作用
處理樹上的一些相關問題。比如——維護樹上區間,樹上路徑等等。
區間我們想到了線段樹,樹上路徑想到了LCA,但是它們都有一個特點——連續。線段樹只能維護連續區間,LCA路徑也是不間斷的。所以為了便於處理,我們要對這個圖重新標號,以便查詢。怎麼標呢?我們可以想到——在樹鏈上操作LCA路徑,那麼路徑也是要連貫的,也就是說重鏈上的編號要連貫,所以我們重新編號的時候是在dfs序的基礎上遵循先遍歷重兒子的原則。
例題
思路
可以很容易發現——操作1、2都需要走一遍x到y的路徑,操作3、4都需要操作以x為根的子樹。所以我們先思考怎麼遍歷這些區間——
首先遍歷x到y的路徑,我們亦容易想到LCA——兩個點同時往上跳,直到某個值相同,可以一起操作。所以我們的思路就是:兩個點不在同一條鏈就往鏈頭的父親節點跳,在同一條鏈上就直接處理。而處理方法也很簡單——因為全程都在鏈上以連續的新節點編號來操作,所以線段樹維護區間距離就很方便了,完全不受樹剖影響地敲一個基本的建樹、查詢、區間修改+延遲標記的程式碼就可以了。
而對於操作3和4,以x為根的子樹,顯然編號也是連續的——畢竟編號時的最基本原則還是dfs遍歷。但是有一個小問題——我們知道以x為根的子樹最小的編號是x的編號,但是最大的編號我們並不知道,如果遍歷一遍來找的話複雜度就會比較高了。所以——這是我們在初始化樹剖的時候要儲存下來的一個變數——以x為根的子樹的最大的節點編號。也就是程式碼中的son[ ]。
程式碼
#include<algorithm> #include<iostream> #include<cstdlib> #include<cstring> #include<cstdio> #define Rint register int #define mem(a,b) memset(a,(b),sizeof(a)) #define Temp template<typename T> using namespace std; typedef long long LL; Temp inline void read(T &x){ x=0;T w=1,ch=getchar(); while(!isdigit(ch)&&ch!='-')ch=getchar(); if(ch=='-')w=-1,ch=getchar(); while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^'0'),ch=getchar(); x=x*w; } #define mid ((l+r)>>1) #define lson rt<<1,l,mid #define rson rt<<1|1,mid+1,r #define len (r-l+1) const int maxn=200000+10; int n,m,r,mod; //見題意 int e,beg[maxn],nex[maxn],to[maxn],w[maxn],wt[maxn]; //鏈式前向星陣列,w[]、wt[]初始點權陣列 int a[maxn<<2],laz[maxn<<2]; //線段樹陣列、lazy操作 int son[maxn],id[maxn],fa[maxn],cnt,dep[maxn],siz[maxn],top[maxn]; //son[]重兒子編號,id[]新編號,fa[]父親節點,cnt dfs_cl ock/dfs序,dep[]深度,siz[]子樹大小,top[]當前鏈頂端節點 int res=0; //查詢答案 inline void add(int x,int y){//鏈式前向星加邊 to[++e]=y; nex[e]=beg[x]; beg[x]=e; } //-------------------------------------- 以下為線段樹 inline void pushdown(int rt,int lenn){ laz[rt<<1]+=laz[rt]; laz[rt<<1|1]+=laz[rt]; a[rt<<1]+=laz[rt]*(lenn-(lenn>>1)); a[rt<<1|1]+=laz[rt]*(lenn>>1); a[rt<<1]%=mod; a[rt<<1|1]%=mod; laz[rt]=0; } inline void build(int rt,int l,int r){ if(l==r){ a[rt]=wt[l]; if(a[rt]>mod)a[rt]%=mod; return; } build(lson); build(rson); a[rt]=(a[rt<<1]+a[rt<<1|1])%mod; } inline void query(int rt,int l,int r,int L,int R){ if(L<=l&&r<=R){res+=a[rt];res%=mod;return;} else{ if(laz[rt])pushdown(rt,len); if(L<=mid)query(lson,L,R); if(R>mid)query(rson,L,R); } } inline void update(int rt,int l,int r,int L,int R,int k){ if(L<=l&&r<=R){ laz[rt]+=k; a[rt]+=k*len; } else{ if(laz[rt])pushdown(rt,len); if(L<=mid)update(lson,L,R,k); if(R>mid)update(rson,L,R,k); a[rt]=(a[rt<<1]+a[rt<<1|1])%mod; } } //---------------------------------以上為線段樹 inline int qRange(int x,int y){ int ans=0; while(top[x]!=top[y]){//當兩個點不在同一條鏈上 if(dep[top[x]]<dep[top[y]])swap(x,y);//把x點改為所在鏈頂端的深度更深的那個點 res=0; query(1,1,n,id[top[x]],id[x]);//ans加上x點到x所在鏈頂端 這一段區間的點權和 ans+=res; ans%=mod;//按題意取模 x=fa[top[x]];//把x跳到x所在鏈頂端的那個點的上面一個點 } //直到兩個點處於一條鏈上 if(dep[x]>dep[y])swap(x,y);//把x點改為所在鏈頂端的深度更淺的那個點 res=0; query(1,1,n,id[x],id[y]);//這時再加上此時兩個點的區間和即可 ans+=res; return ans%mod; } inline void updRange(int x,int y,int k){//同上 k%=mod; while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]])swap(x,y); update(1,1,n,id[top[x]],id[x],k); x=fa[top[x]]; } if(dep[x]>dep[y])swap(x,y); update(1,1,n,id[x],id[y],k); } inline int qSon(int x){ res=0; query(1,1,n,id[x],id[x]+siz[x]-1);//子樹區間右端點為id[x]+siz[x]-1 return res; } inline void updSon(int x,int k){//同上 update(1,1,n,id[x],id[x]+siz[x]-1,k); } inline void dfs1(int x,int f,int deep){//x當前節點,f父親,deep深度 dep[x]=deep;//標記每個點的深度 fa[x]=f;//標記每個點的父親 siz[x]=1;//標記每個非葉子節點的子樹大小 int maxson=-1;//記錄重兒子的兒子數 for(Rint i=beg[x];i;i=nex[i]){ int y=to[i]; if(y==f)continue;//若為父親則continue dfs1(y,x,deep+1);//dfs其兒子 siz[x]+=siz[y];//把它的兒子數加到它身上 if(siz[y]>maxson)son[x]=y,maxson=siz[y];//標記每個非葉子節點的重兒子編號 } } inline void dfs2(int x,int topf){//x當前節點,topf當前鏈的最頂端的節點 id[x]=++cnt;//標記每個點的新編號 wt[cnt]=w[x];//把每個點的初始值賦到新編號上來 top[x]=topf;//這個點所在鏈的頂端 if(!son[x])return;//如果沒有兒子則返回 dfs2(son[x],topf);//按先處理重兒子,再處理輕兒子的順序遞迴處理 for(Rint i=beg[x];i;i=nex[i]){ int y=to[i]; if(y==fa[x]||y==son[x])continue; dfs2(y,y);//對於每一個輕兒子都有一條從它自己開始的鏈 } } int main(){ read(n);read(m);read(r);read(mod); for(Rint i=1;i<=n;i++)read(w[i]); for(Rint i=1;i<n;i++){ int a,b; read(a);read(b); add(a,b);add(b,a); } dfs1(r,0,1); dfs2(r,r); build(1,1,n); while(m--){ int k,x,y,z; read(k); if(k==1){ read(x);read(y);read(z); updRange(x,y,z); } else if(k==2){ read(x);read(y); printf("%d\n",qRange(x,y)); } else if(k==3){ read(x);read(y); updSon(x,y); } else{ read(x); printf("%d\n",qSon(x)); } } }
例題2
思路
這是邊轉點,顧名思義就是把邊權轉化為點權來用。而又因為每個點的父親是唯一的,所以我們都是把邊權給了深度更大的那個點。
在這個題裡,我們用邊轉點的樹鏈剖分來求LCA,剩下的和本題題解思路一樣,用Kruskal求最大生成樹。。。
程式碼
#include<bits/stdc++.h>
#define maxn 10005
#define maxm 50005
using namespace std;
int read()
{
register int x = 0, f = 1, ch = getchar();
while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
return x * f;
}
int n, m;
struct node
{
int u, v, w;
bool operator < (const node &x) const
{
return x.w < w;
}
}g[maxm];
int fa[maxn];
int get(int x)
{
if(fa[x] == x) return x;
return fa[x] = get(fa[x]);
}
struct edge
{
int to, w, nxt;
edge(){}
edge(int tt, int ww, int nn)
{
to = tt, w = ww, nxt = nn;
}
}e[maxn << 1];
int k = 0, head[maxn];
void add(int u, int v, int w)
{
e[k] = edge(v, w, head[u]);
head[u] = k++;
}
int dep[maxn], size[maxn], son[maxn], val[maxn], fa_[maxn];
void dfs_1(int u)//樹剖初始化1
{
size[u] = 1;
for(register int i = head[u]; ~i; i = e[i].nxt)
{
register int v = e[i].to, w = e[i].w;
if(v == fa_[u]) continue;
dep[v] = dep[u] + 1;
fa_[v] = u;
dfs_1(v);
size[u] += size[v];
if(size[v] > size[son[u]]) son[u] = v;
}
}
int top[maxn], dfn[maxn], tot = 0;
void dfs_2(int u, int tp)//樹剖初始化2
{
top[u] = tp;
dfn[u] = ++tot;
if(son[u]) dfs_2(son[u], tp);
for(register int i = head[u]; ~i; i = e[i].nxt)
{
register int v = e[i].to, w = e[i].w;
if(v != fa_[u] && v != son[u]) dfs_2(v, v);
val[dfn[v]] = w;
}
}
int road[maxn << 2];
void build(int p, int l, int r)//線段樹建樹
{
if(l == r)
{
road[p] = val[l];
return;
}
register int mid = l + r >> 1;
build(p << 1, l, mid);
build(p << 1 | 1, mid + 1, r);
road[p] = min(road[p << 1], road[p << 1 | 1]);
}
int ask(int p, int l, int r, int ls, int rs)//線段樹查詢
{
if(ls <= l && r <= rs)
return road[p];
}
register int mid = l + r >> 1, ans = 1 << 30;
if(ls <= mid) ans = min(ans, ask(p << 1, l, mid, ls, rs));
if(rs > mid) ans = min(ans, ask(p << 1 | 1, mid + 1, r, ls, rs));
return ans;
}
void work(int u, int v)//樹剖基本操作
{
register int ans = 1 << 30;
register int tmp1 = u, tmp2 = v;
while(top[u] != top[v])
{
if(dep[top[u]] > dep[top[v]]) swap(u, v);
ans = min(ans, ask(1, 1, n, dfn[top[v]], dfn[v]));
v = fa_[top[v]];
}
if(dep[u] > dep[v]) swap(u, v);
ans = min(ans, ask(1, 1, n, dfn[son[u]], dfn[v]));
printf("%d\n", ans);
}
int main()
{
n = read(), m = read();
for(register int i = 1; i <= n; i++)
fa[i] = i;
for(register int i = 1; i <= m; i++)
g[i].u = read(), g[i].v = read(), g[i].w = read();
sort(g + 1, g + 1 + m);
memset(head, -1, sizeof head);
register int cnt = 0;//最大生成樹開始
for(register int i = 1; i <= m; i++)
{
register int u = g[i].u, v = g[i].v, w = g[i].w;
if(get(u) == get(v)) continue;
fa[get(u)] = get(v);
add(u, v, w);
add(v, u, w);
cnt++;
if(cnt == n - 1) break;//省時處理
}
for(int i = 1; i <= n; i++)//這裡一定要開for!!!QAQ
if(!size[i]) dfs_1(i);
for(int i = 1; i <= n; i++)//這裡也一定要開for!!!QAQ
if(!top[i]) dfs_2(i, i);
val[1] = 1 << 30;//賦值
build(1, 1, n);
register int q, u, v;
q = read();
while(q--)
{
u = read(), v = read();
if(get(u) != get(v))//判定不連通情況
{
puts("-1");continue;
}
work(u, v);
}
}