咱们一般求最小生成树有两种常见的算法——\(Prim\)和\(Kruskal\)算法,今天先总结最小生成树概念和比较简单的\(Prim\)算法ios
一个有 \(n\) 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的全部 \(n\) 个结点,而且有保持图连通的最少的边。
——来自百度百科算法
咱们用比较通俗的语言来说:(百度百科的解释实在是太鬼了,我这个明白人都看着迷糊)数组
给定一张包含\(n\)个点\(m\)条边的连通带权无向图\(G\),咱们从中选出\(n-1\)条边,使得这\(n\)个点互相连通优化
连通以后,发现所选的\(n-1\)条边和图中的\(n\)个点,构成了一棵树,咱们称之为:“生成树”spa
咱们对这棵生成树的全部边权求和,获得一个数\(size\),称之为:“生成树大小”code
如今咱们就能够字面上理解“最小生成树”是什么意思了——一个图的全部生成树中\(size\)值最小的那一个,就是这个图的最小生成树(\(Minimum\) \(Spanning\) \(Tree\),简称\(MST\))blog
其实你能够更简单的理解为:最小生成树就是连通\(n\)个点所花费的最小代价图片
如今我将展现个人画图技巧,举一个例子:ip
如今给定这张无向图\(G\),咱们的任务是求出它的\(MST\)内存
使用肉眼观察法,咱们获得的\(MST\)应该是选择\((1,3),(4,3),(2,4)\)这三条边,权值和为\(5\),也就是咱们只花了\(5\)的代价,就把全部点连起来了
经过枚举全部生成树能够发现:不论怎么选,上述方案的权值和必定是最小的,因此它的最小生成树大小为\(5\),包含\((1,3),(4,3),(2,4)\)这三条边
定理:任意一棵最小生成树必定包含无向图中权值最小的边
定理证实:
反证法,假设无向图\(G\)的最小生成树不包含这个权值最小的边(把这个最小权值边设为\(e\)且\(e\)连通点\(x,y\),权值为\(z\))
把\(e\)添加到不包含\(e\)的这棵最小生成树里去,必定会造成一个环,而且环上的每一条\(e\)之外的边的权值必定比\(z\)大
此时,咱们随便去掉一条\(e\)之外的边,整个图仍然连通成一棵树,且权值和\(size\)更小(由于加入\(z\),去掉一个大于\(z\)的权)
发现这与一开始的假设矛盾,因此假设不成立,原命题成立,证毕。
这里咱们抛开正确性证实,只谈原理(正确性证实是计算机科学家的事,咱们须要的是了解与应用)
最初,\(Prim\)算法仅肯定\(1\)号节点已经在最小生成树中
\(一、\)设已经选入最小生成树的节点集合为\(T\),没有选入的节点集合为\(S\)
\(二、\)找到一条边\(e(x,y,z)\)(链接\(x,y\),权值为\(z\)),使得\(x\in S,y\in T\)且\(z\)最小
\(三、\)在集合\(S\)中删除\(y\),加入到集合\(T\)中,并累加\(z\)到\(size\)中
\(四、\)重复上述操做,直到集合\(S\)为空为止,此时\(size\)就是最小生成树的大小
具体到代码里能够这么写:
维护一个数组\(dis\),若节点\(i\in S\)则\(dis[i]\)表示节点\(i\)与集合\(T\)中的节点之间权值最小的边的权值,若\(i\in T\)则\(dis[i]\)表示\(i\)被加入\(T\)时选出的最小边的权值
发现这好像与\(dijkstra\)算法要维护的东西有点像:
\(dijkstra\)算法维护一个未知最短路的点到已知最短路的点的最短距离,每次肯定到达一个点的最短路,用于更新其余未知点到已知点的最短距离
而\(Prim\)算法维护一个未加入最小生成树的点到已加入最小生成树的点的边权最小值,每次选择一个点加入到最小生成树,更新边权最小值
因此咱们能够用一个数组\(vis\)来标记一个节点是否属于集合\(S\),每次从未标记的节点中选出\(dis\)值最小的,把它标记,加入集合\(T\),扫描这个点的全部出边,更新另外一个端点的\(dis\)值
最后,当集合\(S\)为空时,算法结束,最小生成树大小为\(\sum_{x=2}^{n}dis[x]\),你也能够直接在选出边权最小值的时候直接累加,再也不求和
另外,\(Prim\)算法的时间复杂度为\(O(n^2)\),算不上太优秀,可是由于有求最小值的操做,因此咱们能够把\(dis\)数组换成一个小根堆,把时间复杂度优化到\(O(mlogn)\)
#include<cstring> #include<cstdio> #define N 5010 using namespace std; int f[N][N],dis[N],vis[N],m,n,total; void prim(int x){ memset(dis,0x7f,sizeof(dis));//赋初值无穷大 memset(vis,1,sizeof(vis));//把全部点标记为在集合S中 dis[x]=0;//1号点dis为0 for(int i=1;i<=n;i++){ int k=0; for(int j=1;j<=n;j++) if(vis[j]!=0&&(dis[j]<dis[k])) k=j;//集合S中最小的dis值的点为k vis[k]=0;//把k加入到最小生成树 total+=dis[k];//把选择边的边权累加到total里 for(int j=1;j<=n;j++)//用节点k更新dis中其余的值 if(vis[j]!=0&&(f[k][j]<dis[j])) dis[j]=f[k][j];//更新一个不在最小生成树中的点j的dis值 } } int main(){ scanf("%d%d",&n,&m); memset(f,0x7f,sizeof(f)); for(int i=1,x,y,z;i<=m;i++){ scanf("%d%d%d",&x,&y,&z);//初始化邻接矩阵 if(f[x][y]<z) continue; f[x][y]=z; f[y][x]=z; } for(int i=1;i<=n;i++) f[i][i]=0; prim(1);//执行Prim算法 printf("%d\n",total); return 0; }
2020/8/15 13:37 update:增长了堆优化\(Prim\)的代码
#include<cstdio> #include<cstring> #include<queue> #include<stack> #include<algorithm> #include<set> #include<map> #include<utility> #include<iostream> #include<list> #include<ctime> #include<cmath> #include<cstdlib> #include<iomanip> typedef long long int ll; inline int read(){ int fh=1,x=0; char ch=getchar(); while(ch<'0'||ch>'9'){ if(ch=='-') fh=-1;ch=getchar(); } while('0'<=ch&&ch<='9'){ x=(x<<3)+(x<<1)+ch-'0';ch=getchar(); } return fh*x; } inline int _abs(const int x){ return x>=0?x:-x; } inline int _max(const int x,const int y){ return x>=y?x:y; } inline int _min(const int x,const int y){ return x<=y?x:y; } inline int _gcd(const int x,const int y){ return y?_gcd(y,x%y):x; } inline int _lcm(const int x,const int y){ return x*y/_gcd(x,y); } const int maxn=100005; struct Edge{ int cost,to; }; //以上均为缺省源,下面是主要部分 int n,m;//n个点m条边的无向图G std::vector<Edge>v[maxn];//vector邻接表建图 //Prim算法 std::priority_queue< std::pair<int,int> >Q; //小根堆优化,第一维存权值,第二维存点标号 int vis[maxn]; inline int Prim(){ int MST=0; vis[1]=1; for(unsigned int i=0;i<v[1].size();i++)//把1号点的全部连边所有入堆 Q.push(std::make_pair(-v[1][i].cost,v[1][i].to));//入堆时取反变成了小根堆 while(Q.size()!=0){ while(vis[Q.top().second]){ Q.pop(); if(Q.size()==0) return MST;//若是这里堆中没有元素,下一次循环时会访问无效内存,致使RE //因此特判一下,若是堆中没有元素,那么说明已经完成了最小生成树的求解,返回MST便可 } int x=Q.top().second; MST-=Q.top().first;//注意存边的时候取反了,这里再取反 Q.pop(); vis[x]=true;//把点x加入最小生成树 for(unsigned int i=0;i<v[x].size();i++){ int y=v[x][i].to,z=v[x][i].cost; if(!vis[y]) Q.push(std::make_pair(-z,y)); //若是y不在生成树里,才会入堆 } } return MST; } int main(){ n=read(),m=read(); for(int i=0,x,y,z;i<m;i++){ x=read(),y=read(),z=read(); v[x].push_back((Edge){z,y}); v[y].push_back((Edge){z,x});//建图 } int ans=Prim(); printf("%d\n",ans); return 0; }
关于最小生成树\(Prim\)算法的分享就到这里,感谢您的阅读!