最小生成树:
一个有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; }