最小生成樹:
一個有 n 個結點的連通圖的生成樹是原圖的極小連通子圖,且包含原圖中的所有 n 個結點,並且有保持圖連通的最少的邊。簡單來說就是有且僅有n個點n-1條邊的連通圖。
而最小生成樹就是最小權重生成樹的簡稱,即所有邊的權值之和最小的生成樹。
最小生成樹問題一般有以下兩種求解方式。
一、Prim算法
參考了Feynman的博客
Prim算法通常以鄰接矩陣作為儲存結構。
算法思路:以頂點為主導地位,從起始頂點出發,通過選擇當前可用的最小權值邊把頂點加入到生成樹當中來:
1.從連通網絡N={V,E}中的某一頂點U0出發,選擇與它關聯的具有最小權值的邊(U0,V),將其頂點加入到生成樹的頂點集合U中。
2.以后每一步從一個頂點在U中,而另一個頂點不在U中的各條邊中選擇權值最小的邊(U,V),把它的頂點加入到集合U中。如此繼續下去,直到網絡中的所有頂點都加入到生成樹頂點集合U中為止。
模板題鏈接:Prim算法求最小生成樹
朴素版時間復雜度O(n²)算法模板:
#include <iostream> #include <cstdio> #include <algorithm> #include <cstring> using namespace std; const int N = 500+10; int n,m; int g[N][N],dis[N],vis[N]; void prim() { memset(dis,0x1f,sizeof dis); dis[1]=0; for(int j=1;j<=n;j++) { int min_len=2e+9,k; for(int i=1;i<=n;i++) { if(!vis[i]&&dis[i]<min_len) { min_len=dis[i]; k=i; } } vis[k]=1; for(int i=1;i<=n;i++) { if(!vis[i]&&dis[i]>g[k][i]) dis[i]=g[k][i]; } } } int main() { scanf("%d%d",&n,&m); memset(g,0x1f,sizeof g); for(int i=1;i<=m;i++) { int u,v,w;scanf("%d%d%d",&u,&v,&w); g[u][v]=g[v][u]=min(g[u][v],w); //因為有重邊,所以取min } prim(); int ans=0; for(int i=1;i<=n;i++)ans+=dis[i]; if(ans>1e7)printf("impossible\n"); else printf("%d\n",ans); return 0; }
與Dijkstra類似,Prim算法也可以用堆優化,優先隊列代替堆,優化的Prim算法時間復雜度O(mlogn)模板(圖的存儲方式為前向星):
void Prim_heap(int point) { memset(dis,0x1f,sizeof(dis)); priority_queue<pair<int,int> > q; dis[point]=0; q.push(make_pair(0,1)); while(!q.empty()) { int k=q.top().second; q.pop(); v[k]=1; for(int i=h[k];i!=-1;i=edge[i].next) { int to=edge[i].to,w=edge[i].w; if(!v[to]&&dis[to]>w) { dis[to]=w; q.push(make_pair(-dis[to],to)); //優先隊列大根堆變小根堆小騷操作:只需一個‘-’號; } } } for(int i=1;i<=n;i++)if(dis[i]==0x1f1f1f1f)flag=false; //判斷是否不存在最小生成樹 return ; }
二、Kruskal算法
相比於Prim算法,更常用的還是Kruskal,其原因在於Kruskal算法模板的代碼量小而且思路易理解。
算法思路:先構造一個只含 n 個頂點、而邊集為空的子圖,把子圖中各個頂點看成各棵樹上的根結點,之后,從網的邊集 E 中選取一條權值最小的邊,若該條邊的兩個頂點分屬不同的樹,則將其加入子圖,即把兩棵樹合成一棵樹,反之,若該條邊的兩個頂點已落在同一棵樹上,則不可取,而應該取下一條權值最小的邊再試之。依次類推,直到森林中只有一棵樹,也即子圖中含有 n-1 條邊為止。
步驟:
-
新建圖G,G中擁有原圖中相同的節點,但沒有邊;
-
將原圖中所有的邊按權值從小到大排序;
-
從權值最小的邊開始,如果這條邊連接的兩個節點於圖G中不在同一個連通分量中,則添加這條邊到圖G中;
-
重復3,直至圖G中所有的節點都在同一個連通分量中。
簡單來說就是以邊為主導地位,每次選擇權值最小的邊,判斷該邊連接的兩點是否連通,若不連通,則合並兩點(合並操作以並查集實現)。記錄合並的次數,當次數等於n-1時結束。
模板題鏈接:Kruskal算法求最小生成樹
代碼如下:時間復雜度O(mlogm)
#include <iostream> #include <algorithm> #include <cstdio> #include <cstring> using namespace std; const int N = 100000+10, M = 200000+10; struct Edge{ int u,v,w; bool operator < (const Edge &E)const { return w<E.w; } }edge[M]; int fa[N]; int n,m,cnt,ans; int find(int x) { if(fa[x]==x)return x; else return fa[x]=find(fa[x]); } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++)fa[i]=i; for(int i=1;i<=m;i++) { int a,b,c;scanf("%d%d%d",&a,&b,&c); edge[i].u=a;edge[i].v=b;edge[i].w=c; } sort(edge+1,edge+m+1); for(int i=1;i<=m;i++) { int u=find(edge[i].u),v=find(edge[i].v),w=edge[i].w; if(u!=v) { cnt++; fa[u]=v; ans+=w; } } if(cnt==n-1)printf("%d\n",ans); else printf("impossible\n"); return 0; }
三、Prim,Prim_heap,Kruskal算法時間復雜度比較
參考了G機器貓的博客





結論:
1.Prim在稠密圖中比Kruskal優,在稀疏圖中比Kruskal劣。
2.Prim_heap在任何時候都有令人滿意的的時間復雜度,但是代價是空間消耗極大。(以及代碼很復雜>_<)
但值得說一下的是,時間復雜度並不能反映出一個算法的實際優劣。
競賽題一般給的都是稀疏圖,選擇Prim_heap即可;如果覺得代碼量太大,想要在Prim與Kruskal算法中選一個,那就選擇Kruskal算法。
