最小生成树算法——Prim & Kruskal总结与代码实例
Prim算法
Prim算法的思想是:对于原始图G(V,E),建立一个集合S,从图的某个顶点开始(顶点可以是指定顶点,也可以是随机的顶点),逐步从点集V-S中选择到S中顶点距离最小的顶点加入S,并把该条最短路径加入最小生成树。
在实现的过程中,Prim算法与求解单源最短路径算法Dijkstra算法几乎一致,唯一不同之处在于最短距离数组d[MAXN]的含义与处理方式。下面给出邻接矩阵版的prim算法的实现例子。
代码实例
/*codeup 622A*/
/*稠密图的最小生成树算法—prim算法的应用*/
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int MAXN = 110;
const int INF = 1000000000;
int G[MAXN][MAXN];
int d[MAXN];
bool visit[MAXN] = {false};
int N;
int prim(){
int ans = 0;
fill(d,d+MAXN,INF);
d[1] = 0; //序号1-N,默认从1开始
for(int i=1;i<=N;i++){ //循环N次
int u = -1,MIN = INF;
for(int j=1;j<=N;j++){
if(visit[j] == false && d[j] < MIN){
u = j;
MIN = d[j];
}
}
if(u == -1)
return -1;
visit[u] = true;
ans += d[u]; //将最小的边加入
for(int v=1;v<=N;v++){ //遍历通过u连接的路径
if(visit[v] == false && G[u][v] != INF && G[u][v] < d[v]){ //顶点v未被访问且u->v有边且通过u可以使得v到最小生成树节点集合距离变小
d[v] = G[u][v]; //更新距离
}
}
}
return ans;
}
int main(){
fill(G[0],G[0]+MAXN*MAXN,INF);
int c1,c2,cost;
while(cin>>N){
if(N == 0)
break;
for(int i=0;i<N*(N-1)/2;i++){
cin>>c1>>c2>>cost;
G[c1][c2] = G[c2][c1] = cost;
}
cout<<prim()<<endl;
}
return 0;
}
由于Prim算法与Dijkstra算法一致,复杂度均为O(N^2),故顶点的数量会影响算法的性能,所以Prim算法常被用于顶点数量较少而边数量较多(即稠密图)的情况,若是稀疏图,则应该尽量使用下面介绍的Kruskal算法
Kruskal算法
Kruskal 算法也是求解图的最小生成树的常用算法,与Prim算法不同,Kruskal算法采用的是"边贪心"的策略,即以图中的边为主导。其算法过程是首先将图中所有的顶点各自看成独立的连通块,然后对图中的各边按照边权从小到大的顺序进行排列,然后按照排序后的顺序遍历图的各个边。当处理一条边时,检查与该边相连的两个顶点是否属于同一个连通块,如果属于,则跳过该边,反之则将该边加入最小生成树,然后让最小生成树的边数量加1。当最小生成树的边的数量为顶点数量减一时,跳出遍历循环。结束边的遍历循环后,检查最小生成树的边的数量,若不是N-1,则表明原图不连通。
在实现Kruskal算法时有两个关键问题:一是如何知晓边的两个顶点属于同一连通块(如何表示连通块),二是如何将一条边加入最小生成树(如何表示一个最小生成树)。以上两个问题可以使用一个简单的结构来描述——并查集。判断是否属于同一连通块,就找寻两个顶点在并查集中的根结点是否一致,边加入最小生成树其实就是两个集合的合并过程。
下面给出基于并查集实现的Kruskal算法
代码实例
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN = 200; //定义最大顶点数与最大边数
const int MAXE = 10010;
//边集定义
struct edge{
int u,v; //起点,终点编号
int cost; //边权
}E[MAXE];
//cmp()函数,用以sort()
bool cmp(edge a,edge b){
return a.cost < b.cost;
}
int father[MAXN]; //顶点的并查集
//寻找根结点函数
int findFather(int x){
int a = x;
while(x != father[x]){
x = father[x];
}
//路径压缩,其实也可以不做
while(a != father[a]){
int z = a;
a = father[a];
father[z] = x; //将查询顶点到根结点路径上所有的顶点的父节点均记为根节点
}
return x; //返回根节点编号
}
//Kruskal算法
int Kruskal(int n, int m){
int ans = 0;
int num_edge = 0;
for(int i=0;i<n;i++){
father[i] = i; //并查集初始化
}
sort(E,E+m,cmp); //边结构体数组排序
for(int i=0;i<m;i++){ //从小到大遍历边
int faU = findFather(E[i].u);
int faV = findFather(E[i].v);
if(faU != faV){ //如果边的两个顶点不再同一连通块
father[faU ] = faV; //合并
ans += E[i].cost; //边权累加
num_edge++;
if(num_edge == n-1) //最小生成树边数已达顶点数减一,跳出
break;
}
}
if(num_edge != n-1)
return -1; //不连通
else
return ans;
}
int main(){
int n,m;
cin>>n>>m;
for(int i=0;i<m;i++){
cin>>E[i].u>>E[i].v>>E[i].cost;
}
int ans = Kruskal(n,m);
cout<<ans<<endl;
return 0;
}