先来说一下Prim算法的基本思路:
1.任意找到图中的一个顶点,作为最小生成树的根,进行标记(已被加入树)开始扩展
2.从已扩展的结点中找到权值最小的点(已被扩展,可以保证其与生成树是连通的),记录并标记。
3.将与该点相连通的所有结点进行扩展
4.循环执行2,3步,直到所有结点都被加入到生成树
为此,需要维护几个变量来保存已有状态和记录当前状态:
color[MAX]; 用于记录所有结点的访问状态(对应算法思路第1,2步中的标记),WHITE表示未被访问,BLACK表示已被加入生成树
MAP[MAX][MAX];邻接矩阵,用于记录图中连通结点之间的权值
d[MAX]; 用于记录权值最小边的权值 eg.d[v]表示点v与生成树连通的最小权值
p[MAX]; 用于记录生成树中各个结点的父节点 eg.p[v]表示生成树中v的父节点
下面看代码:
#include <iostream>
#include <algorithm>
#include <cstdlib>
#define max 1<<10
#define INF 1<<12
#define white 1
#define black 0
using namespace std;
int n,m;
int path[max][max];
int prim(){
int p[max];
int d[max];
int color[max];
int mink;
int u;
for(int i=0;i<m;i++){
p[i]=-1;
d[i]=INF;
color[i]=white;
}
u=0;
d[u]=0;
p[0]=INF;
while(true){
u=-1;
mink=INF;
for(int i=0;i<m;i++){//找出扩展的点
if(d[i]<mink&&color[i]!=black){
u=i;
mink=d[i];
}
}
if(u==-1)break;//没有找到点,说明都已被扩展
color[u]=black;
for(int i=0;i<m;i++){//扩展该点
if(color[i]!=black&&path[u][i]!=INF){//i点可扩展且存在与u的路径
if(path[u][i]<d[i]){
d[i]=path[u][i];
p[i]=u;
}
}
}
}
int result=0;
for(int i=0;i<m;i++){
if(p[i]!=INF)
result+=path[i][p[i]];
}
return result;
}
以上是最经典的Prim算法,使用邻接矩阵实现。
对于复杂度:我们需要遍历图的所有顶点来确定d最小的顶点u,且整个算法的遍历次数与顶点数相等,因此算法复杂度为 O(|V|²)。
对于小规模数据,可以用此算法解决,但对于数据较大的,在算法竞赛中就会超时。
下面举一道题为例:
洛谷P3366:
题目描述
如题,给出一个无向图,求出最小生成树,如果该图不连通,则输出orz
输入输出格式
输入格式:
第一行包含两个整数N、M,表示该图共有N个结点和M条无向边。(N<=5000,M<=200000)
接下来M行每行包含三个整数Xi、Yi、Zi,表示有一条长度为Zi的无向边连接结点Xi、Yi
输出格式:
输出包含一个数,即最小生成树的各边的长度之和;如果该图不连通则输出orz
其中:
对于20%的数据:N<=5,M<=20
对于40%的数据:N<=50,M<=2500
对于70%的数据:N<=500,M<=10000
对于100%的数据:N<=5000,M<=200000
我们看到,对于100%,使用复杂度O(|V|²)的算法很可能会超时。因此需要对算法进行优化
分析算法,我们很容易了解到,时间主要花费在了挑选最小权值点和扩展上,对于挑选最小权值点,我们可以用通过维护一个优先队列来加快我们的取点速度。而对于扩展,我们可以用邻接表的存储方式,减少对无权点(不连通点)遍历的时间
下面是具体的题解代码:
#include <iostream>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <queue>
#include <vector>
#define INF 1<<12
#define white 1
#define gray 2
#define black 3
using namespace std;
int n, m;
int path[5005][5005];
int p[5005];
int d[5005];
int color[5005];
int result;
struct node {
int loc;
int len;
};
struct comp {
bool operator()(node a, node b) {
return a.len>b.len;
}
};
vector<node> road[5005];//邻接表
int sign;
void prim() {
int min;
int u;
priority_queue<node, vector<node>, comp> buf;//记录扩展出去的最小路径的堆
d[0] = 0;
p[0] = 0;
node temp = { 0,0 };
buf.push(temp);
while (!buf.empty()) {
u = buf.top().loc;//用优先队列减少取点时间
//min=buf.top().len;
buf.pop();
color[u] = black;
for (int i = 0; i<road[u].size(); i++) {//邻接表取点,减少对无权点的遍历
int v = road[u][i].loc;
if (color[v] == black)continue;
int templen = road[u][i].len;
if (templen<d[v]) {
d[v] = templen;
p[v] = u;
temp.len = d[v];
temp.loc = v;
buf.push(temp);
}
}
}
for (int i = 1; i<n; i++) {
if (p[i] == -1) {//判断是否连通
sign = 1;
break;
}
result += d[i];
}
return;
}
int main() {
cin >> n >> m;
node a;
for (int i = 0; i<m; i++) {
int x, y, z;
cin >> x >> y >> z;
path[x-1][y-1] = z;
a.loc = y-1;
a.len = z;
road[x-1].push_back(a);
path[y-1][x-1] = z;
a.loc = x-1;
a.len = z;
road[y-1].push_back(a);
}
for (int i = 0; i<n; i++) {
p[i] = -1;
d[i] = INF;
color[i] = white;
}
prim();
if (sign)cout << "orz" << endl;
else {
cout << result << endl;
}
return 0;
}
本题有一个要求是判断是否连通,对此,只要最后遍历每个结点,检查其根是否是生成树内结点即可完成。