Kruskal重構樹學習筆記+BZOJ3732 Network
今天學了Kruskal重構樹,似乎很有意思的樣子~
先看題面:
BZOJ
題目大意:$n$ 個點 $m$ 條無向邊的圖,$k$ 個詢問,每次詢問從 $u$ 到 $v$ 的所有路徑中,最長的邊的最小值。
$1\leq n\leq 15000,1\leq m\leq 30000,1\leq k\leq 20000$。
我相信你們看見這題的想法和我一樣:
貨車運輸!最小生成樹上LCA一下就行了!時間復雜度 $O(mlogm+n\alpha(n)+klogn)$。(我的LCA是樹鏈剖分)
但是這題有另一種做法:Kruskal重構樹。
(這種用到路徑最長邊值等一般都能用Kruskal重構樹)
Kruskal重構樹是什麽?和Kruskal求最小生成樹有什麽關系?
我們先來看一下Kruskal重構樹的一些性質。
Kruskal重構樹是由原圖的 $n$ 個點,新添加的 $n-1$ 個點和 $2n-2$ 條邊構成的樹。
(以下討論的Kruskal重構樹都是最小Kruskal重構樹,最大Kruskal重構樹反之亦然)
下面是一個圖和它的Kruskal重構樹:(圓點是原圖中的點,方點是新加的點)
它的性質有:
- 一棵二叉樹
- 葉子結點都是原圖中的點,沒有點權;非葉子結點都是新加的點,有點權
- 舊點 $u,v$ 兩點間(不包括 $u,v$)的所有節點(都是新點)的點權最大值為原圖中 $u,v$ 兩點間所有路徑的最長邊的最小值
- 新點構成一個堆(不一定是二叉堆),在最小Kruskal重構樹中是大根堆
- 結合性質3和4,舊點 $u,v$ 兩點的LCA(是新點)權值為原圖中 $u,v$ 兩點間所有路徑的最長邊的最小值
比如點 $1,3$ 的LCA權值為 $3$,恰好是原圖中 $1,3$ 兩點間所有路徑的最長邊的最小值(邊 $(2,3)$)。
Kruskal重構樹怎麽求呢?
- 找到一條邊權最小的,且沒有被遍歷過的邊。
- 如果這條邊連接的兩個點目前沒有在一個聯通塊,那麽新建一個節點,點權為該邊的邊權,左右兒子設為這兩個點的最遠祖先。(通過設置兒子為最遠祖先合並聯通塊且不破壞性質)
- 重復1,2直到遍歷完所有的邊。
畫個圖了解一下,下圖中紅點是該邊連接的兩個點,綠點和綠邊是添加的點和邊:(點我食用這張圖會更佳)
如何用代碼實現?像普通的Kruskal一樣,維護一個路徑壓縮並查集,其中 $fa$ 數組不止止是判斷在哪個集合,更是標記在重構樹上的父親。這也是為什麽要路徑壓縮而不是按秩合並的原因,因為這樣可以更快求得最遠祖先。
每處理一條邊,先判連通性,如果不連通那麽把這兩個點的最遠祖先的 $fa$ 都設成新加的點,同時連邊即可。註意,不能反過來,否則重構樹在並查集上的結構會被破壞。
至於舊點之間,怎麽連邊,連那些邊?參見性質2。
(其實就是不用連邊,我好啰嗦啊)
回到原題:建出最小Kruskal重構樹,每次LCA一下即可。
這裏順便說一下,如果原圖不保證聯通怎麽辦,如何預處理?(以下的代碼中我也判了兩點不連通的情況)
兩點不連通可以用建Kruskal重構樹中的並查集知道。而預處理,每次如果一個點沒有dfs到,那麽從這個點的最遠祖先dfs一遍(也可以用並查集知道)。
為什麽要最遠祖先呢?因為這樣是對的且省時間,均攤 $O(\alpha(n))$。自己感受一下
(洛谷上貨車運輸那題是有不連通的圖的測試點,我寫重構樹時第一次沒有從最遠祖先開始dfs,感受一下:)
時間復雜度也是 $O(mlogm+n\alpha(n)+klogn)$。但是學了一個新算法不是很好嗎?
(讀者:浪費時間,散了散了,吃雞去了)
代碼:(附帶判斷不連通)
1 #include<bits/stdc++.h> 2 using namespace std; 3 struct edge1{ //一開始的邊,方便排序 4 int u,v,w; 5 bool operator<(const edge1 e)const{ 6 return w<e.w; 7 } 8 }e1[30030]; 9 struct edge2{ //重構樹上的邊(雖然只有兩個兒子但這樣寫舒服) 10 int to,nxt; 11 }e2[30030]; //重構樹的點數是原圖的約2倍,但是沒有雙向邊 12 int n,m,q,el1,el2,cnt,root; 13 int u_fa[30030],w[30030],head[30030]; //u_fa是並查集的fa,w是新節點的權值 14 int dep[30030],fa[30030],son[30030],size[30030],top[30030]; 15 inline void add1(int u,int v,int w){ //原圖加邊 16 e1[++el1]=(edge1){u,v,w}; 17 } 18 inline void add2(int u,int v){ //重構樹加邊 19 e2[++el2]=(edge2){v,head[u]}; 20 head[u]=el2; 21 } 22 int getfa(int x){ //路徑壓縮 23 return x==u_fa[x]?x:u_fa[x]=getfa(u_fa[x]); 24 } 25 void dfs1(int u,int f){ 26 fa[u]=f; 27 dep[u]=dep[f]+1; 28 size[u]=1; 29 int maxson=-1; 30 for(int i=head[u];i;i=e2[i].nxt){ 31 int v=e2[i].to; 32 if(v==fa[u]) continue; 33 dfs1(v,u); 34 size[u]+=size[v]; 35 if(size[v]>maxson) maxson=size[v],son[u]=v; 36 } 37 } 38 void dfs2(int u,int topf){ 39 top[u]=topf; 40 if(!son[u]) return; 41 dfs2(son[u],topf); 42 for(int i=head[u];i;i=e2[i].nxt){ 43 int v=e2[i].to; 44 if(v==fa[u] || v==son[u]) continue; 45 dfs2(v,v); 46 } 47 } 48 int calc(int u,int v){ 49 if(getfa(u)!=getfa(v)) return -1; //如果不連通 50 while(top[u]!=top[v]){ 51 if(dep[top[u]]<dep[top[v]]) swap(u,v); 52 u=fa[top[u]]; 53 } 54 return dep[u]<dep[v]?w[u]:w[v]; //LCA的權值 55 } 56 int main(){ 57 scanf("%d%d",&n,&m); 58 for(int i=1;i<=m;i++){ 59 int u,v,w; 60 scanf("%d%d%d",&u,&v,&w); 61 add1(u,v,w); 62 } 63 sort(e1+1,e1+m+1); //從小到大排序 64 for(int i=1;i<=2*n-1;i++) u_fa[i]=i; 65 cnt=n; 66 for(int i=1;i<=m;i++){ 67 int u=e1[i].u,v=e1[i].v; //操作的兩個點 68 u=getfa(u);v=getfa(v); 69 if(u==v) continue; //已經聯通,跳過 70 w[++cnt]=e1[i].w; //新建一個點 71 u_fa[u]=u_fa[v]=cnt; //設置父親! 72 add2(cnt,u);add2(cnt,v); //加邊(不需要雙向邊) 73 } 74 for(int i=1;i<=2*n-1;i++) 75 if((w[i] || i<=n) && !dep[i]) //如果這個點存在並且沒有被搜過 76 //解釋一下這個條件,w[i]是表示這個點有沒有權值,如果有就是新點(為什麽要判斷這個呢,因為圖不連通,可能點個數並沒有達到2n-1);i<=n表示這個是不是舊點;!dep[i]表示沒被搜過 77 dfs1(u_fa[i],0),dfs2(u_fa[i],0); //那就搜唄 78 scanf("%d",&q); 79 for(int i=1;i<=q;i++){ 80 int u,v; 81 scanf("%d%d",&u,&v); 82 printf("%d\n",calc(u,v)); 83 } 84 }Kruskal重構樹
另外今年的NOI2018D1T1正解也是Kruskal生成樹,以後再慢慢杠吧,留個坑等著補題解。
Kruskal重構樹學習筆記+BZOJ3732 Network