最小生成樹
模板題:【模板】最小生成樹
求最小生成樹的邊權和。
Prim
這似乎是我最早學的最小生成樹演算法。也是忘的最早的
首先注意到,由 \(n\) 個節點和 $ n-1 $ 條邊構成的 連通圖 一定是樹。那麼只需要選 \(n-1\) 條邊使圖連通,求最小代價。不難發現只要保證結果不出現環就可能是答案。
考慮貪心求解,對於當前生成樹構成的連通塊,每次向外擴充代價最小的邊,併合並對應的節點即可。注意,如果兩個節點都在連通塊內,那麼它們之間不能再加邊,否則一定會出現環。
暴力做的話可以做到 $ O(n^2+m) $ ,用單調佇列維護可以做到 $ O( (n+m)\log n) $ ,具體實現類似於dijkstra
CODE
struct node{
int x;ll y;
bool operator<(node an)const{
return y>an.y;
}
};
priority_queue<node>q;
void prim(){
memset(dis,0x3f,sizeof(dis));
dis[1]=0;
q.push({1,0});
while(!q.empty()){
node u=q.top();q.pop();
if(vis[u.x])continue;
vis[u.x]=1;
ans+=u.y;
for(int i=head[u.x];i;i=e[i].nxt){
int v=e[i].to;
if(dis[v]>e[i].val){
dis[v]=e[i].val;
q.push({v,e[i].val});
}
}
}
}
Kruskal
一種更常用的演算法。
由於只要保證生成樹上沒有環,在此基礎上求最小代價和。將所有邊按邊權升序排序,依次遍歷,如果兩點不在同一連通塊就合併,並查集維護。複雜度 $ O(m\log m) $ 。可以發現這就是最優答案。
CODE
struct edge{
int x,y,val;
}e[N];
bool cmp(edge fir,edge sec){
return fir.val<sec.val;
}
int find(int x){
if(x==fa[x])return x;
else return fa[x]=find(fa[x]);
}
inline void merge(int x,int y){
int fx=find(x),fy=find(y);
fa[fx]=fy;
}
void kruskal(){
sort(e+1,e+m+1,cmp);
for(int i=1;i<=m;i++){
if(find(e[i].x)!=find(e[i].y)){
ans+=e[i].val;
merge(e[i].x,e[i].y);
}
}
}
Boruvka
最近剛學的菠蘿演算法。
初始時,我們把每個節點看做一個連通塊,進行如下操作:
-
對於每個連通塊,尋找連線它和另一個不同的連通塊的最小邊
-
將最小邊連線的兩個連通塊合併,並統計答案(這裡每次合併前都要判斷是否屬於同一連通塊)
-
如果所有節點都合併到同一連通塊,退出
有點像前兩種演算法結合起來。正確性是顯然的,因為每次連通塊向外連邊一定是當前最優也是以後最優的。每次合併連通塊數量至少減半,複雜度為 $ O(m\log n) $
CODE
struct node{
int x,y;
}minn[N];
int find(int x){
if(x==fa[x])return x;
else return fa[x]=find(fa[x]);
}
inline void merge(int x,int y){
int fx=find(x),fy=find(y);
fa[fx]=fy;
}
bool check(){
for(int i=2;i<=n;i++){
if(find(i)!=find(i-1))return 1;
}
return 0;
}
void Boruvka(){
while(check()){
for(int i=1;i<=n;i++)minn[i].y=inf;
for(int i=1;i<=n;i++){
int fx=find(i);
for(int j=head[i];j;j=e[j].nxt){
if(fx!=find(e[j].to)&&e[j].val<minn[fx].y)minn[fx]={e[j].to,e[j].val};
}
}
for(int i=1;i<=n;i++){
int fx=find(i);
if(fx!=find(minn[fx].x)){
merge(fx,minn[fx].x);
ans+=minn[fx].y;
}
}
}
}
Boruvka 在實現難度和常數方面不如前兩種演算法,但是在某一類題目中有明顯優勢:給定 \(n\) 個點,每個點都有權值,並且兩兩之間可以任意連邊,邊權以某種方式定義且與兩個端點的權值有關,求最小生成樹的邊權和。可以參考練習題。
練習題
限速
題意
給定 \(n\) 個點, \(m\) 條帶權邊的連通圖,首先選擇 $ n-1 $ 條邊構成一棵樹,其次,可以操作一次使一條邊權加1或減1,使的生成樹的最大邊權 恰好 為 \(k\) ,求最小操作次數。 $ n,m \le 2 \times 10^5 ,k\le 1\times 10^9 $
題解
為敘述方便,我們定義權值不超過 \(k\) 的邊為A邊,其餘為B邊,考慮選擇的邊有兩種情況:
-
只有A邊
-
存在B邊
第一種情況代價最小的方案是把邊權最大的A邊加到 \(k\) ;第二種情況最小代價是把B邊都減至 \(k\) 。
先判斷只有A邊能否構成生成樹,如果可以,一定能選擇邊權最大的A邊作為候選答案。考慮加入一條B邊,在形成的生成樹一定會出現一個環,斷掉環上任意一條A邊即可。 $ ans = \min \{ k- \max a_i \ , \min b_i -k \} $
如果A邊不能構成生成樹,直接跑最小生成樹使最大權最小。
排列最小生成樹 (pmst)
題意
給定一個 \(1\) 到 \(n\) 的排列 \(p\) 。構造一個有 \(n\) 個節點的無向完全圖,定義節點 \(i\) 和節點 \(j\) 的邊權為 $ | p_i-p_j | \times | i-j | $ ,求最小生成樹的邊權和。$ n\le 5 \times 10^4 $
題解
發現神秘結論即可
首先注意到答案不會很大,因為一定有這樣的一種構造:生成樹是一條編號連續的鏈,這樣每條邊的權值都不會超過 $ n $ 。再考慮正解,發現每個點只有 $ O(\sqrt N) $ 條邊是有意義的,其它邊權都超過 \(n\) 了。所以只要先按 \(i\) 排序,取相鄰的 $ \sqrt n $ 個點建邊,再按 $ p_i $ 排序,重複操作,跑 kruskal
。排序的時候建議桶排, sort
和不優良的實現都會T飛的。
星際聯邦
題意
一個 $ n $ 個節點的完全圖,第 $ i $ 個節點的權值為 $ u_i $ (可能為負),從 $ i $ 向 $ j $ 的邊權為 $ u_j-u_i $ (要求 $ i < j $ ),求最小生成樹邊權和。 $ n\le 3 \times 10^5 $
題解
考慮 Boruvka
怎麼做,難點在於 $ O(n) $ 的複雜度求出每個連通塊向外的最小邊。發現對於節點 $ i $ ,與其不屬於同一連通塊且邊權最小的點只可能有兩個 : 向前最小的 $ u_j ( j < i ,f_j \neq f_i ) $ 和向後最大的 $ u_j ( j>i,f_j \neq f_i ) $ 。發現 $ f_i \neq f_j $ 很難做到,這裡積累一個 trick
,記錄 $ minn[0][i] $ 為 $ [1,i) $ 中最小值的編號 ,$ minn[1][i] $ 為與 $ minn[0][i] $ 屬於不同連通塊的最小值的編號,這樣就可以快速求出與 $ i $ 不在同一連通塊的最大值。對於 $ j>i $ 同理。實現有些細節,其它沒了。