淺談最小生成樹
———
\(\rm BiuBiu\_Miku\)
1.一些概念
· 樹:在一個圖中,滿足邊數等於點數減一的條件。(如圖1所示)
· 生成樹:在一個連通圖中,截取一個子圖,此子圖滿足樹的性質,且通過每一個節點的樹稱為生成樹。(如圖2所示)
· 最小生成樹:在一個包含 \(n\) 個節點的加權連通圖中,所有邊的邊權之和最小的樹,且通過每一個節點的樹,即為最小生成樹。(如圖3所示)
2.例題引入(題皆出自洛谷)
【模板】最小生成樹
題目描述
給出一個無向圖,求出最小生成樹,如果該圖不連通,則輸出 \(orz\)。
輸入格式
第一行包含兩個整數 \(N,M\),表示該圖共有 \(N\) 個結點和 \(M\) 條無向邊。
接下來 \(M\) 行每行包含三個整數 \(X_i,Y_i,Z_i\) 表示有一條長度為 \(Z_i\) 的無向邊連接結點 \(X_i,Y_i\) 。
輸出格式
如果該圖連通,則輸出一個整數表示最小生成樹的各邊的長度之和。如果該圖不連通則輸出 \(orz\)。
輸入輸出樣例
輸入
4 5
1 2 2
1 3 2
1 4 3
2 3 4
3 4 3
輸出
7
3.算法實現
關於算法的實現一般有兩種算法,第一種稱為 \(Kruskal\) ,第二種稱為 \(Prim\)
實現方法一.\(Kruskal\) 時間復雜度 \(O( MlogM )\) (\(M\)為邊數)
\(Kruskal\) 是一種利用並查集來實現最小生成樹的辦法,其算法流程大致為。
先將讀進來的邊按照邊權排序,然后利用並查集建立關系,一但發現如果兩個點不存在關系,那么就建這條邊,不然的話就不建,最后把建了邊的邊權統計起來就好了。
為什么這樣就可以找到最小生成樹呢?因為我們已經將邊權排序過了,也就是說越前面的邊他的邊權就越小,因此我們就可以直接通過簡單的步驟得到最小生成樹了!
以上面的題目的樣例來做例子,來模擬一下該算法全過程:
1.排序,排序后得到以下的數據
1 2 2
1 3 2
1 4 3
3 4 3
2 3 4
2.發現 \(1\) 到 \(2\) 不連通,於是建邊,如下圖所示:
3.發現 \(1\) 到 \(3\) 不連通,於是建邊,如下圖所示:
4.發現 \(1\) 到 \(4\) 不連通,於是建邊,如下圖所示:
5.發現 \(3\) 到 \(4\) 已經連通,於是不建邊。
6.發現 \(2\) 到 \(3\) 已經聯通,於是不建邊。
7.經統計,一共建的邊的邊權之和為 \(7\) 於是就輸出 \(7\) 。
\(Code:\)
#include<bits/stdc++.h>
using namespace std;
int f[200005],m;
struct edge{
int a,b,lo;
}E[200005]; //邊
int n,ans;
bool cmp(edge x,edge y){return x.lo<y.lo;} //排序
int getfather(int k){ //並查集
if(f[k]==k)return k;
return f[k]=getfather(f[k]);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)f[i]=i;
for(int i=1;i<=m;i++)scanf("%d%d%d",&E[i].a,&E[i].b,&E[i].lo);
sort(E+1,E+1+m,cmp); //排序
for(int i=1;i<=m;i++){
if(getfather(E[i].a)!=getfather(E[i].b)){ //判斷兩個點是否連通
ans+=E[i].lo; //若不連通建邊
f[getfather(E[i].a)]=getfather(E[i].b);
n--; //減掉一個未連通點
}
if(n==1){ //若全部連通就輸出答案
printf("%d\n",ans);
return 0;
}
}
printf("orz\n"); //否則輸出orz
return 0;
}
實現方法二.\(Prim\) (時間復雜度根據不同情況,不同計算)
\(Prim\) 是一種利用不斷尋找最小值來實現找到最小生成樹的方法。
具體來說就是以某一個點為起點(一般選擇節點 \(1\) 為起點),尋找能走的邊中邊權(代價)最小的邊,再將此點收入囊中,也就是看做是一段子圖。
然后更新能走的邊所花費的代價,因為當我加入一個節點后,該節點也可以通到別的點,而且從此新節點出發走原來可以走的邊,可能存在更小代價.
例如:若此時 \(A\) 已經與 \(B\) 已經連接,\(A\) 到 \(C\) 代價為 \(3\) , \(B\) 存在一條邊到 \(C\) 的代價為 \(1\) ,那么此時就可以更新當前可以走的邊。
以上面的題目的樣例來做例子,來模擬一下\(Prim\) 算法全過程:
1.選擇節點 \(1\) 為起點,並發現當前 \(1\) 到 \(2\) 和 \(1\) 到 \(3\) 都是當前能走的邊中邊權最小的,於是任意走一條即可(這里選擇走 \(1\) 到 \(2\) ),如下圖所示。
2.當前能走的邊中發現 \(1\) 到 \(3\) 是邊權最小的邊,於是走此邊,如下圖所示。
3.當前能走的邊中,發現當前 \(1\) 到 \(4\) 和 \(3\) 到 \(4\) 都是當前能走的邊中邊權最小的,於是任意走一條即可(這里選擇走 \(1\) 到 \(4\) ),如下圖所示。
4.找到最小生成樹,經統計,走過的邊權總和為 \(7\) 於是輸出 \(7\)。
\(Code:\) (這里用鄰接矩陣存圖,復雜度為 \(O(N^2)\) N表示點的數量 )
#include<bits/stdc++.h>
using namespace std;
int n,mmin=INT_MAX,dis[200005],ans,w[5005][5005],walk,m;
bool vis[200005]; //vis表示該邊是否被訪問過
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
w[i][j]=1e9; //矩陣初始化
for(int i=1;i<=m;i++){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
w[a][b]=min(c,w[a][b]);
w[b][a]=min(c,w[b][a]); //鄰接矩陣存圖
}
for(int i=1;i<=n;i++)dis[i]=w[1][i]; //初始化從起點開始能到達的邊,能到達就賦值走到該點要付出的代價,否則為1e9
dis[1]=0;
vis[1]=true;
for(int i=2;i<=n;i++){
mmin=1e9;
for(int j=2;j<=n;j++)
if(dis[j]<mmin&&!vis[j]){ //找到當前所有能走的邊的最小值
mmin=dis[j];
walk=j;
}
ans+=mmin; //走這條邊
vis[walk]=true; //標記已經走過
for(int j=2;j<=n;j++)if(!vis[j])dis[j]=min(dis[j],w[walk][j]); //更新能走的邊花費的價值
}
printf("%d\n",ans);
return 0;
}
4.題目推薦
[USACO05MAR]Out of Hay S:相當於模板(雙倍經驗,豈不美哉)。
[USACO3.1]最短網絡 Agri-Net:本題用 \(for\) 建邊后跑流程即可。
公路修建:本題要運用兩點之間的距離公式 \(\sqrt{( x_1 - x_2 )^2+( y_1 - y_2 )^2}\) 來計算邊權,然后跑流程就好了,本題推薦使用 \(Prim\) 來實現。
\(PS\) :以上題目適合初學者食用。
5.結語
以上便是此博客的全部內容,其主要適用於沒學過最小生成樹的人食用,有什么寫得不好也歡迎大佬在評論區指出,感謝大佬的閱讀。