Prim算法
1).输入:一个加权连通图,其中顶点集合为V,边集合为E;
2).初始化:Vnew= {x},其中x为集合V中的任一节点(起始点),Enew= {},为空;
3).重复下列操作,直到Vnew= V:
1. 在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
2. 将v加入集合Vnew中,将<u, v>边加入集合Enew中;
4).输出:使用集合Vnew和Enew来描述所得到的最小生成树。
prim算法和dijkstra算法求最短路很像,基于贪心,遍历n次,每次选中一个点:
dijkstra算法是求从起点s到所有点的最短距离(dis数组的含义),每次选取的 点t 是到 起点s 距离最近的点,然后再通过 点t 去更新其它点到起点s的距离;
dis[i]=min(dis[i],dis[t]+g[t][i]);
而prim算法每次选取的点是到集合最近的点(dis数组的含义),这个集合就是已经加入到最小生成树的点的集合。因此,每次选取的 点t 是到 集合 距离最近的点(集合中许多点,其中一个的距离dis),然后再更新其它点到集合的最近的距离,其实也就是更新其它点到 点t 的最短距离就行了,因为到集合内的其它点都已经更新过了,此时dis数组已经是到除集合内 点t 外的最短距离了。
if(!vis[i]) dis[i]=min(dis[i],g[t][i]);
因此,prim算法也分为朴素算法和堆优化算法:
- 朴素prim算法适合解决稠密图的最小生成树,采用邻接矩阵存储,时间复杂度O(n2);
- 堆优化算法适合解决稀疏图,采用邻接表存储,优化的地方也是找距离集合最近的点,通过最小堆直接取堆顶元素,时间复杂度O(mlogn),但是稀疏图一般采用kruskal算法。
题目描述
给定一个n个点m条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。
给定一张带权的无向图G=(V, E),其中V表示图中点的集合,E表示图中边的集合,n=|V|,m=|E|。
由V中的全部n个顶点和E中n-1条边构成的无向连通子图被称为G的一棵生成树,其中边的权值之和最小的生成树被称为无向图G的最小生成树。
输入格式
第一行包含两个整数n和m。
接下来m行,每行包含三个整数u,v,w,表示点u和点v之间存在一条权值为w的边。
输出格式
共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。
数据范围
1≤n≤500,
1≤m≤105,
图中涉及边的边权的绝对值均不超过10000。
输入样例:
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例:
6
朴素prim算法
#include <iostream>
#include <cstring>
using namespace std;
const int N=510,INF=0x3f3f3f3f;
int g[N][N];
int dis[N];//这里dis的含义是每个点到集合的最短距离
bool vis[N];
int n,m;
int prim()
{
memset(dis,0x3f,sizeof dis);
dis[1]=0; //随便从一个起点开始
int num=n,res=0;
while (num--) {//对于连通图,必须要循环n次才能到所有顶点,获得最小生成树,非连通图会中途break
int t=0;//结点是从1开始的,t=0,此时说明还没开始待选
for (int i=1;i<=n;i++) {
if (!vis[i] && (!t || dis[i]<dis[t]) ) t=i;
}
if (dis[t]==INF) return -1; //存在顶点不可达,说明是不存在生成树
vis[t]=true; //否则,顶点t加入集合
res+=dis[t]; //计算生成树的代价,第一次循环时必为事先初始化的1号顶点,dis为0,相当于不加
//开始更新到集合(顶点t)的距离
for (int i=1;i<=n;i++) {
if (!vis[i] && dis[i]>g[t][i]) dis[i]=g[t][i];
"只更新不在集合中的点,维护dis数组的含义,所以dis[t]不会被更新,就算自环是负值也不会被更新。"
}
}
return res; //返回最小生成树的代价
}
int main()
{
scanf("%d%d",&n,&m);
memset(g,0x3f,sizeof g);
int a,b,c;
while (m--) {
scanf("%d%d%d",&a,&b,&c); //a->b的边,权值为c
g[a][b]=g[b][a]=min(g[a][b],c); //无向图的存储,两条边
}
int t=prim();//如果不存在最小生成树,t=-1,否则t是最小生成树的代价。
t==-1?puts("impossible"):printf("%d",t);
return 0;
}
题目和dijkstra算法相比,边权也可能是负值了,
但是自环不用考虑,因为我们先将该点加入集合,然后再更新不再集合中的点,所以自环可以忽略。
重边的处理,同前面,取最小权值的边即可。
记得这是个无向图,存边时要存储双向: g[a][b]=g[b][a]=min(g[a][b],c);
堆优化prim算法
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
#define ss second
#define ff first
typedef pair<int,int> PII; //<距离,顶点>
priority_queue< PII,vector<PII>,greater<PII> >heap;
const int N=510,M=2e5+10; //无向图,边数要乘2
int h[N],e[M],w[M],ne[M],idx;
int dis[N];
bool vis[N]; //判断是否已经加入生成树的集合
int n,m;
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int prim()
{
memset(dis,0x3f,sizeof dis);
dis[1]=0;
heap.push({dis[1],1});//本来还应该剩下的顶点都放进去,然后进行更新就行了,但是stl中的堆不能实现按关键字更新,
int num=n,res=0; //用插入代替更新,所以,剩下的点就不放入了,更新到时再放入
while (heap.size() && num) { //这里为什么这么写,原因同dijkstra
auto t=heap.top();
heap.pop();
int v=t.ss,d=t.ff; "此时取出的d必不可能是INF,因为前面加入堆的原因就是它被更新过"
if (vis[v]) continue; //如果v已经在生成树中了,本次循环无效,开始下一次,后面不再执行
vis[v]=true,res+=d,num--; //否则,加入生成树
//修改未在生成树中的点到集合的最短距离
for (int i=h[v];i!=-1;i=ne[i]) {
int j=e[i];//v->e[i]的边,权值为w[i]
if (!vis[j] && dis[j] > w[i]) dis[j]=w[i],heap.push({dis[j],j});
} "只有当v到其它顶点可达或者距离更小,才会更新,因此每次取出的d都不是INF,当都不可达时,队列就为null了"
}
"判断是因为什么原因退出?是因为堆空,还是有效循环num次完了"
"因为堆空退出说明图是非连通图,存在不可达点,此时num>0;但是如果num==0,说明已经得到了生成树。"
return num?-1:res;
}
int main()
{
memset(h,-1,sizeof h); "切记,邻接表存储时要初始化h数组"
scanf("%d%d",&n,&m);
int a,b,c;
while (m--) {
scanf("%d%d%d",&a,&b,&c);
add(a,b,c); add(b,a,c);
}
int t=prim();
t==-1?puts("impossible"):printf("%d",t);
return 0;
}
分析优先队列空的原因:
连通图,所有顶点都遍历过了,加入生成树了,vis[j]都为true,不能进行更新了;
非连通图,存在不可达顶点,不能更新了。