這裏介紹最小生成樹的兩種方法:Prim和Kruskal。
二者區別:Prim在稠密圖中比Kruskal優,在稀疏圖中比Kruskal劣。Prim是以更新過的節點的連邊找最小值,Kruskal是直接將邊排序。
二者其實都是運用貪心的思路
洛谷數據:
Prim:
我的以爲Prim和最短路中的dijkstra很像,因爲速度問題,因此這裏我用鏈式前向星存圖。Prim的思想是將任意節點做爲根,再找出與之相鄰的全部邊(用一遍循環便可),再將新節點更新並以此節點做爲根繼續搜,維護一個數組:dis,做用爲已用點到未用點的最短距離。c++
證實:Prim算法之因此是正確的,主要基於一個判斷:對於任意一個頂點v,鏈接到該頂點的全部邊中的一條最短邊(v, vj)必然屬於最小生成樹(即任意一個屬於最小生成樹的連通子圖,從外部鏈接到該連通子圖的全部邊中的一條最短邊必然屬於最小生成樹)算法
具體算法流程圖解以下:
注意:inline和register爲一點點常數優化,不要的話也能夠過,不理解的同窗刪掉便可數組
#include<bits/stdc++.h> using namespace std; #define re register #define il inline il int read() { re int x=0,f=1;char c=getchar(); while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();} while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar(); return x*f; }//快讀,不理解的同窗用cin代替便可 #define inf 123456789 #define maxn 5005 #define maxm 200005 struct edge { int v,w,next; }e[maxm<<1]; //注意是無向圖,開兩倍數組 int head[maxn],dis[maxn],cnt,n,m,tot,now=1,ans; //已經加入最小生成樹的的點到沒有加入的點的最短距離,好比說1和2號節點已經加入了最小生成樹,那麼dis[3]就等於min(1->3,2->3) bool vis[maxn]; //鏈式前向星加邊 il void add(int u,int v,int w) { e[++cnt].v=v; e[cnt].w=w; e[cnt].next=head[u]; head[u]=cnt; } //讀入數據 il void init() { n=read(),m=read(); for(re int i=1,u,v,w;i<=m;++i) { u=read(),v=read(),w=read(); add(u,v,w),add(v,u,w); } } il int prim() { //先把dis數組附爲極大值 for(re int i=2;i<=n;++i) { dis[i]=inf; } //這裏要注意重邊,因此要用到min for(re int i=head[1];i;i=e[i].next) { dis[e[i].v]=min(dis[e[i].v],e[i].w); } while(++tot<n)//最小生成樹邊數等於點數-1 { re int minn=inf;//把minn置爲極大值 vis[now]=1;//標記點已經走過 //枚舉每個沒有使用的點 //找出最小值做爲新邊 //注意這裏不是枚舉now點的全部連邊,而是1~n for(re int i=1;i<=n;++i) { if(!vis[i]&&minn>dis[i]) { minn=dis[i]; now=i; } } ans+=minn; //枚舉now的全部連邊,更新dis數組 for(re int i=head[now];i;i=e[i].next) { re int v=e[i].v; if(dis[v]>e[i].w&&!vis[v]) { dis[v]=e[i].w; } } } return ans; } int main() { init(); printf("%d",prim()); return 0; }
Kruskal:
Kruskal算法的思想比Prin好理解一些。先把邊按照權值進行排序,用貪心的思想優先選取權值較小的邊,並依次鏈接,若出現環則跳過此邊(用並查集來判斷是否存在環)繼續搜,直到已經使用的邊的數量比總點數少一便可。markdown
證實:剛剛有提到:若是某個連通圖屬於最小生成樹,那麼全部從外部鏈接到該連通圖的邊中的一條最短的邊必然屬於最小生成樹。因此不難發現,當最小生成樹被拆分紅彼此獨立的若干個連通份量的時候,全部可以鏈接任意兩個連通份量的邊中的一條最短邊必然屬於最小生成樹ide
具體算法流程圖解以下:
並查集詳解
#include<bits/stdc++.h> using namespace std; #define re register #define il inline il int read() { re int x=0,f=1;char c=getchar(); while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();} while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar(); return x*f; } struct Edge { int u,v,w; }edge[200005]; int fa[5005],n,m,ans,eu,ev,cnt; il bool cmp(Edge a,Edge b) { return a.w<b.w; } //快排的依據(按邊權排序) il int find(int x) { while(x!=fa[x]) x=fa[x]=fa[fa[x]]; return x; } //並查集循環實現模板,及路徑壓縮,不懂並查集的同窗能夠戳一戳代碼上方的「並查集詳解」 il void kruskal() { sort(edge,edge+m,cmp); //將邊的權值排序 for(re int i=0;i<m;i++) { eu=find(edge[i].u), ev=find(edge[i].v); if(eu==ev) { continue; } //若出現兩個點已經聯通了,則說明這一條邊不須要了 ans+=edge[i].w; //將此邊權計入答案 fa[ev]=eu; //將eu、ev合併 if(++cnt==n-1) { break; } //循環結束條件,及邊數爲點數減一時 } } int main() { n=read(),m=read(); for(re int i=1;i<=n;i++) { fa[i]=i; } //初始化並查集 for(re int i=0;i<m;i++) { edge[i].u=read(),edge[i].v=read(),edge[i].w=read(); } kruskal(); printf("%d",ans); return 0; }