最小生成树:Prim和Kruskal算法
在一个无向连通图中,如果存在一个连通子图包含原图中的所有的结点和部分边,且这个子路不存在回路,那么我们称这个子图为原图的一棵生成树。在带权图中,所有的生成树中边权的和最小的那颗(或几棵)被称为最小生成树。——摘自王道
下面将从两种最常见的算法Prim和Kruskal来讲解最小生成树,并进行比较
1.Prim算法
Prim算法从任意一个顶点开始,每次选择一个与当前顶点集最近的一个顶点,并将两顶点之间的边加入到树中。Prim算法在找当前最近顶点时使用到了贪婪算法。
#include<iostream>
#define INF 10000
using namespace std;
const int N = 6;
bool visit[N];
int dist[N] = { 0, };
int graph[N][N] = { {INF,7,4,INF,INF,INF}, //INF代表两点之间不可达
{7,INF,6,2,INF,4},
{4,6,INF,INF,9,8},
{INF,2,INF,INF,INF,7},
{INF,INF,9,INF,INF,1},
{INF,4,8,7,1,INF}
};
int prim(intcur)
{
int index = cur;
int sum = 0;
int i = 0;
int j = 0;
cout << index << " ";
memset(visit,false, sizeof(visit));
visit[cur] = true;
for(i = 0; i < N; i++)
dist[i] = graph[cur][i];//初始化,每个与a邻接的点的距离存入dist
for(i = 1; i < N; i++)
{
int minor = INF;
for(j = 0; j < N; j++)
{
if(!visit[j] && dist[j] < minor) //找到未访问的点中,距离当前最小生成树距离最小的点
{
minor = dist[j];
index = j;
}
}
visit[index] = true;
cout << index << " ";
sum += minor;
for(j = 0; j < N; j++)
{
if(!visit[j] && dist[j]>graph[index][j]) //执行更新,如果点距离当前点的距离更近,就更新dist
{
dist[j] = graph[index][j];
}
}
}
cout << endl;
return sum; //返回最小生成树的总路径值
}
int main()
{
cout << prim(0) << endl;//从顶点a开始
return 0;
}
代码参考点击跳转
可以看到,Prim算法主要操作是通过遍历顶点来完成的,每次都要寻找一个最小距离的顶点,因此可以使用堆来优化。相关资料之后有时间补上。
时间复杂度:O(V^2)
堆优化后时间复杂度:O((V + E) log(V)) = O(E log(V))
Prim算法适用于稠密图
2.Kruskal算法
图片源自点击跳转
Kruskal算法首先需要对所有权重边做从小到大排序。将排序好的权重边依次加入到最小生成树中,如果加入时产生回路就跳过这条边,加入下一条边。当所有结点都加入到最小生成树中之后,就找出了最小生成树,其思想本质也是贪婪算法。在实际使用中,可使用并查集来实现操作。
下面摘取王道中的一道基础例题作为入门:
题目描述:
某省调查乡村交通状况,得到的统计表中列出了任意两村庄间的距离。省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可),并要求铺设的公路总长度为最小。请计算最小的公路总长度。
输入:
测试输入包含若干测试用例。每个测试用例的第1行给出村庄数目N ( < 100 );随后的N(N-1)/2行对应村庄间的距离,每行给出一对正整数,分别是两个村庄的编号,以及此两村庄间的距离。为简单起见,村庄从1到N编号。
当N为0时,输入结束,该用例不被处理。
输出:
对每个测试用例,在1行里输出最小的公路总长度。
样例输入:
3
1 2 1
1 3 2
2 3 4
4
1 2 1
1 3 4
1 4 1
2 3 3
2 4 2
3 4 5
0
样例输出:
3
5
#include <iostream>
#include<algorithm>
#include<cstdio>
#define N 100
using namespace std;
int Tree[N];
int findRoot(int x){//x值的根节点
if(Tree[x]==-1)
return x;
else{
int temp=findRoot(Tree[x]);
Tree[x]=temp;
return temp;
}
}
struct edge{
int a,b;
int cost;//边的权值
bool operator < (const edge &A) const{//重载小于号使其可以按照边的权值从小到大排列
return cost<A.cost;
}
};
struct edge edges[200];
int main()
{
int n;
scanf("%d",&n);
for(int i=0;i<n*(n-1)/2;i++)
scanf("%d %d %d",&edges[i].a,&edges[i].b,&edges[i].cost);
sort(edges,edges+n*(n-1)/2);//按边排序
for(int i=0;i<N;i++)
Tree[i]=-1;
int ans=0;
for(int i=0;i<n*(n-1)/2;i++){
int a=findRoot(edges[i].a);//查a的根节点
int b=findRoot(edges[i].b);//查b的根节点
if(a!=b){//ab两点不是一棵树上的
Tree[a]=b;//合并两个集合
ans+=edges[i].cost;//累加该边权值
}
}
printf("%d\n",ans);
return 0;
}
时间复杂度:O(ElogE)(快排)
Kruskal算法适用于稀疏图