最小生成树模板——Prim算法

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算法也分为朴素算法和堆优化算法:

  1. 朴素prim算法适合解决稠密图的最小生成树,采用邻接矩阵存储,时间复杂度O(n2);
  2. 堆优化算法适合解决稀疏图,采用邻接表存储,优化的地方也是找距离集合最近的点,通过最小堆直接取堆顶元素,时间复杂度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,不能进行更新了;
非连通图,存在不可达顶点,不能更新了。

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值