最小生成樹:
一個有N個點的圖,邊一定是大於等於N-1條的。圖的最小生成樹,就是在這些邊中選擇N-1條出來,連接所有的N個點。這N-1條邊的邊權之和是所有方案中最小的。
最小生成樹用來解決什么問題?
就是用來解決如何用最小的“代價”用N-1條邊連接N個點的問題。
例題:洛谷P3366
最小生成樹共有兩種算法:
prim算法與Kruskal算法
1.prim算法
Prim算法采用與Dijkstra、Bellman-Ford算法一樣的“藍白點”思想:
白點代表已經進入最小生成樹的點,藍點代表未進入最小生成樹的點。
算法思想:
以1為起點生成最小生成樹,min[v]表示藍點v與白點相連的最小邊權。
MST表示最小生成樹的權值之和。
1)初始化:min[v]= ∞(v≠1); min[1]=0;MST=0; b)for (i = 1; i<= n; i++) 1.尋找min[u]最小的藍點u。
2.將u標記為白點
3.MST+=min[u]
4.for 與白點u相連的所有藍點v if (w[u][v]<min[v]) min[v]=w[u][v]; c)
算法結束: MST即為最小生成樹的權值之和
推論:
初始時所有點都是藍點,min[1]=0,min[2、3、4、5]=∞。權值之和MST=0。
第一次循環自然是找到min[1]=0最小的藍點1。將1變為白點,接着枚舉與1相連的所有藍點2、3、4,修改它們與白點相連的最小邊權。
第二次循環是找到min[2]最小的藍點2。將2變為白點,接着枚舉與2相連的所有藍點3、5,修改它們與白點相連的最小邊權。
第三次循環是找到min[3]最小的藍點3。將3變為白點,接着枚舉與3相連的所有藍點4、5,修改它們與白點相連的最小邊權。
最后兩輪循環將點4、5以及邊w[2][5],w[3][4]添加進最小生成樹。
最后權值之和MST=6。
這n次循環,每次循環我們都能讓一個新的點加入生成樹,n次循環就能把所有點囊括到其中;
每次循環我們都能讓一條新的邊加入生成樹,n-1次循環就能生成一棵含有n個點的樹;
每次循環我們都取一條最小的邊加入生成樹,n-1次循環結束后,我們得到的就是一棵最小的生成樹。
這就是Prim采取貪心法生成一棵最小生成樹的原理。 算法時間復雜度:O (n2)。
例題代碼:
#include<iostream> #include<cstdio> #include<cstring> using namespace std; #define inf 0x7fffffff #define maxn 5005 int cost[maxn][maxn],minn,n,m,v2[maxn],tot=1,now,ans; bool v1[maxn]; inline void getcost() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) { cost[i][j]=inf; } } //先將數組賦為極大值 for(int i=1,u,v,w;i<=m;i++) { scanf("%d%d%d",&u,&v,&w); if(cost[u][v]>w) { cost[u][v]=cost[v][u]=w; } } //初始化cost數組 for(int i=1;i<=n;i++) { v2[i]=cost[1][i]; } v1[1]=1; //找出與1節點相連的邊並進行標記 } inline int prim() { while(tot<n) //最小生成樹的概念,邊數比點數小一 { minn=inf; //附上初值 tot++; //已經使用邊數++ for(int i=1;i<=n;i++) { if(!v1[i]&&v2[i]<minn) { minn=v2[i]; now=i; } } //找出與該點相連的最小邊 ans+=minn; //更新答案 for(int i=1;i<=n;i++) { if(v2[i]>cost[now][i]&&!v1[i]) { v2[i]=cost[now][i]; } } v1[now]=1; //在找出與now節點相連的邊並進行標記 } return ans; } int main() { getcost(); printf("%d",prim()); return 0; }
2.Kruskal算法
算法描述:
1.初始化並查集。father[x]=x。
2.tot=0
3.將所有邊用快排從小到大排序。
4.計數器 k=0;
5.for (i=1; i<=M; i++) //循環所有已從小到大排序的邊
if 這是一條u,v不屬於同一集合的邊(u,v)(因為已經排序,所以必為最小)
begin
①合並u,v所在的集合,相當於把邊(u,v)加入最小生成樹。
②tot=tot+W(u,v)
③k++
④如果k=n-1,說明最小生成樹已經生成,則break; end;
6. 結束,tot即為最小生成樹的總權值之和。
Kruskal算法的時間復雜度為O(E*logE),E為邊數。
例題代碼:
#include<iostream> #include<cstring> #include<cstdio> #include<algorithm> using namespace std; int r[5005],n,m,ans,w,e,cnt; struct Edge { int u,v,w; } edge[222222]; bool cmp(Edge a,Edge b)//交換函數 { return a.w<b.w; } int find(int x) { while(x!=r[x]) x=r[x]=r[r[x]]; return x; } void kruskal()//算法精華 { sort(edge,edge+m,cmp); for(int i=0;i<m;i++){ w=find(edge[i].u), e=find(edge[i].v); if(w==e) continue; ans+=edge[i].w; r[e]=w; cnt++; if(cnt==n-1) break; } } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) r[i]=i; for(int i=0;i<m;i++) scanf("%d%d%d",&edge[i].u,&edge[i].v,&edge[i].w); kruskal(); printf("%d",ans); return 0; }
例題2:
洛谷P1546
運用kruskal算法解決
#include<cstdio> #include<iostream> #include<algorithm> using namespace std; struct point { int x; int y; int v; }; point a[9901]; int fat[101]; int n,i,j,x,m,tot,k; int father(int x) { if (fat[x] != x) fat[x] = father(fat[x]); return fat[x]; } void unionn(int x,int y) { int fa = father(x); int fb = father(y); if (fa != fb) fat[fa] = fb; } int cmp(const point&a,const point&b) { if (a.v < b.v) return 1; else return 0; } int main() { cin >> n; for (i = 1; i <= n; i++) for (j = 1; j <= n; j++) { cin >> x; if (x != 0) { m++; a[m].x = i; a[m].y = j; a[m].v = x; } } for (i=1;i<=n;i++) fat[i]=i; sort(a+1,a+m+1,cmp); for(i=1;i<=m;i++) { if (father(a[i].x) != father(a[i].y)) { unionn(a[i].x,a[i].y); tot += a[i].v; k++; } if (k == n-1) break; } cout << tot; return 0; }