图论算法(五)最小生成树Prim算法
最小生成树\(Prim\)算法
我们通常求最小生成树有两种常见的算法——\(Prim\)和\(Kruskal\)算法,今天先总结最小生成树概念和比较简单的\(Prim\)算法
Part 1:最小生成树基础理论
定义
一个有 \(n\) 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 \(n\) 个结点,并且有保持图连通的最少的边。
——来自百度百科
我们用比较通俗的语言来讲:(百度百科的解释实在是太鬼了,我这个明白人都看着迷糊)
给定一张包含\(n\)个点\(m\)条边的连通带权无向图\(G\),我们从中选出\(n-1\)条边,使得这\(n\)个点互相连通
连通之后,发现所选的\(n-1\)条边和图中的\(n\)个点,构成了一棵树,我们称之为:“生成树”
我们对这棵生成树的所有边权求和,得到一个数\(size\),称之为:“生成树大小”
现在我们就可以字面上理解“最小生成树”是什么意思了——一个图的所有生成树中\(size\)值最小的那一个,就是这个图的最小生成树(\(Minimum\) \(Spanning\) \(Tree\),简称\(MST\))
其实你可以更简单的理解为:最小生成树就是连通\(n\)个点所花费的最小代价
现在我将展示我的画图技巧,举一个例子:
现在给定这张无向图\(G\),我们的任务是求出它的\(MST\)
使用肉眼观察法,我们得到的\(MST\)应该是选择\((1,3),(4,3),(2,4)\)这三条边,权值和为\(5\),也就是我们只花了\(5\)的代价,就把所有点连起来了
通过枚举所有生成树可以发现:不论怎么选,上述方案的权值和一定是最小的,所以它的最小生成树大小为\(5\),包含\((1,3),(4,3),(2,4)\)这三条边
定理
定理:任意一棵最小生成树一定包含无向图中权值最小的边
定理证明:
反证法,假设无向图\(G\)的最小生成树不包含这个权值最小的边(把这个最小权值边设为\(e\)且\(e\)连通点\(x,y\),权值为\(z\))
把\(e\)添加到不包含\(e\)的这棵最小生成树里去,一定会形成一个环,并且环上的每一条\(e\)以外的边的权值一定比\(z\)大
此时,我们随便去掉一条\(e\)以外的边,整个图仍然连通成一棵树,且权值和\(size\)更小(因为加入\(z\),去掉一个大于\(z\)的权)
发现这与一开始的假设矛盾,所以假设不成立,原命题成立,证毕。
Part 2:\(Prim\)算法
\(Prim\)算法原理
这里我们抛开正确性证明,只谈原理(正确性证明是计算机科学家的事,我们需要的是了解与应用)
最初,\(Prim\)算法仅确定\(1\)号节点已经在最小生成树中
\(1、\)设已经选入最小生成树的节点集合为\(T\),没有选入的节点集合为\(S\)
\(2、\)找到一条边\(e(x,y,z)\)(连接\(x,y\),权值为\(z\)),使得\(x\in S,y\in T\)且\(z\)最小
\(3、\)在集合\(S\)中删除\(y\),加入到集合\(T\)中,并累加\(z\)到\(size\)中
\(4、\)重复上述操作,直到集合\(S\)为空为止,此时\(size\)就是最小生成树的大小
具体到代码里可以这么写:
维护一个数组\(dis\),若节点\(i\in S\)则\(dis[i]\)表示节点\(i\)与集合\(T\)中的节点之间权值最小的边的权值,若\(i\in T\)则\(dis[i]\)表示\(i\)被加入\(T\)时选出的最小边的权值
发现这好像与\(dijkstra\)算法要维护的东西有点像:
\(dijkstra\)算法维护一个未知最短路的点到已知最短路的点的最短距离,每次确定到达一个点的最短路,用于更新其他未知点到已知点的最短距离
而\(Prim\)算法维护一个未加入最小生成树的点到已加入最小生成树的点的边权最小值,每次选择一个点加入到最小生成树,更新边权最小值
所以我们可以用一个数组\(vis\)来标记一个节点是否属于集合\(S\),每次从未标记的节点中选出\(dis\)值最小的,把它标记,加入集合\(T\),扫描这个点的所有出边,更新另一个端点的\(dis\)值
最后,当集合\(S\)为空时,算法结束,最小生成树大小为\(\sum_{x=2}^{n}dis[x]\),你也可以直接在选出边权最小值的时候直接累加,不再求和
另外,\(Prim\)算法的时间复杂度为\(O(n^2)\),算不上太优秀,但是因为有求最小值的操作,所以我们可以把\(dis\)数组换成一个小根堆,把时间复杂度优化到\(O(mlogn)\)
\(Code\) \(O(n^2)\)
#include<cstring>
#include<cstdio>
#define N 5010
using namespace std;
int f[N][N],dis[N],vis[N],m,n,total;
void prim(int x){
memset(dis,0x7f,sizeof(dis));//赋初值无穷大
memset(vis,1,sizeof(vis));//把所有点标记为在集合S中
dis[x]=0;//1号点dis为0
for(int i=1;i<=n;i++){
int k=0;
for(int j=1;j<=n;j++)
if(vis[j]!=0&&(dis[j]<dis[k])) k=j;//集合S中最小的dis值的点为k
vis[k]=0;//把k加入到最小生成树
total+=dis[k];//把选择边的边权累加到total里
for(int j=1;j<=n;j++)//用节点k更新dis中其他的值
if(vis[j]!=0&&(f[k][j]<dis[j])) dis[j]=f[k][j];//更新一个不在最小生成树中的点j的dis值
}
}
int main(){
scanf("%d%d",&n,&m);
memset(f,0x7f,sizeof(f));
for(int i=1,x,y,z;i<=m;i++){
scanf("%d%d%d",&x,&y,&z);//初始化邻接矩阵
if(f[x][y]<z) continue;
f[x][y]=z;
f[y][x]=z;
}
for(int i=1;i<=n;i++)
f[i][i]=0;
prim(1);//执行Prim算法
printf("%d\n",total);
return 0;
}
2020/8/15 13:37 update:增加了堆优化\(Prim\)的代码
\(Code\) \(O(mlogn)\)
#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
#include<algorithm>
#include<set>
#include<map>
#include<utility>
#include<iostream>
#include<list>
#include<ctime>
#include<cmath>
#include<cstdlib>
#include<iomanip>
typedef long long int ll;
inline int read(){
int fh=1,x=0;
char ch=getchar();
while(ch<'0'||ch>'9'){ if(ch=='-') fh=-1;ch=getchar(); }
while('0'<=ch&&ch<='9'){ x=(x<<3)+(x<<1)+ch-'0';ch=getchar(); }
return fh*x;
}
inline int _abs(const int x){ return x>=0?x:-x; }
inline int _max(const int x,const int y){ return x>=y?x:y; }
inline int _min(const int x,const int y){ return x<=y?x:y; }
inline int _gcd(const int x,const int y){ return y?_gcd(y,x%y):x; }
inline int _lcm(const int x,const int y){ return x*y/_gcd(x,y); }
const int maxn=100005;
struct Edge{
int cost,to;
};
//以上均为缺省源,下面是主要部分
int n,m;//n个点m条边的无向图G
std::vector<Edge>v[maxn];//vector邻接表建图
//Prim算法
std::priority_queue< std::pair<int,int> >Q; //小根堆优化,第一维存权值,第二维存点标号
int vis[maxn];
inline int Prim(){
int MST=0;
vis[1]=1;
for(unsigned int i=0;i<v[1].size();i++)//把1号点的所有连边全部入堆
Q.push(std::make_pair(-v[1][i].cost,v[1][i].to));//入堆时取反变成了小根堆
while(Q.size()!=0){
while(vis[Q.top().second]){
Q.pop();
if(Q.size()==0) return MST;//如果这里堆中没有元素,下一次循环时会访问无效内存,导致RE
//所以特判一下,如果堆中没有元素,那么说明已经完成了最小生成树的求解,返回MST即可
}
int x=Q.top().second;
MST-=Q.top().first;//注意存边的时候取反了,这里再取反
Q.pop();
vis[x]=true;//把点x加入最小生成树
for(unsigned int i=0;i<v[x].size();i++){
int y=v[x][i].to,z=v[x][i].cost;
if(!vis[y]) Q.push(std::make_pair(-z,y)); //如果y不在生成树里,才会入堆
}
}
return MST;
}
int main(){
n=read(),m=read();
for(int i=0,x,y,z;i<m;i++){
x=read(),y=read(),z=read();
v[x].push_back((Edge){z,y});
v[y].push_back((Edge){z,x});//建图
}
int ans=Prim();
printf("%d\n",ans);
return 0;
}
关于最小生成树\(Prim\)算法的分享就到这里,感谢您的阅读!
图论算法(五)最小生成树Prim算法的更多相关文章
- 最小生成树,Prim算法与Kruskal算法,408方向,思路与实现分析
最小生成树,Prim算法与Kruskal算法,408方向,思路与实现分析 最小生成树,老生常谈了,生活中也总会有各种各样的问题,在这里,我来带你一起分析一下这个算法的思路与实现的方式吧~~ 在考研中呢 ...
- 数据结构代码整理(线性表,栈,队列,串,二叉树,图的建立和遍历stl,最小生成树prim算法)。。持续更新中。。。
//归并排序递归方法实现 #include <iostream> #include <cstdio> using namespace std; #define maxn 100 ...
- 最小生成树Prim算法(邻接矩阵和邻接表)
最小生成树,普利姆算法. 简述算法: 先初始化一棵只有一个顶点的树,以这一顶点开始,找到它的最小权值,将这条边上的令一个顶点添加到树中 再从这棵树中的所有顶点中找到一个最小权值(而且权值的另一顶点不属 ...
- 最小生成树--Prim算法,基于优先队列的Prim算法,Kruskal算法,Boruvka算法,“等价类”UnionFind
最小支撑树树--Prim算法,基于优先队列的Prim算法,Kruskal算法,Boruvka算法,“等价类”UnionFind 最小支撑树树 前几节中介绍的算法都是针对无权图的,本节将介绍带权图的最小 ...
- 最小生成树—prim算法
最小生成树prim算法实现 所谓生成树,就是n个点之间连成n-1条边的图形.而最小生成树,就是权值(两点间直线的值)之和的最小值. 首先,要用二维数组记录点和权值.如上图所示无向图: int map[ ...
- HDU1102 最小生成树prim算法
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1102 题意:给出任意两个城市之间建一条路的时间,给出哪些城市之间已经建好,问最少还要多少时间使所有的城 ...
- Highways POJ-1751 最小生成树 Prim算法
Highways POJ-1751 最小生成树 Prim算法 题意 有一个N个城市M条路的无向图,给你N个城市的坐标,然后现在该无向图已经有M条边了,问你还需要添加总长为多少的边能使得该无向图连通.输 ...
- SWUST OJ 1075 求最小生成树(Prim算法)
求最小生成树(Prim算法) 我对提示代码做了简要分析,提示代码大致写了以下几个内容 给了几个基础的工具,邻接表记录图的一个的结构体,记录Prim算法中最近的边的结构体,记录目标边的结构体(始末点,值 ...
- 算法起步之Prim算法
原文:算法起步之Prim算法 prim算法是另一种最小生成树算法.他的安全边选择策略跟kruskal略微不同,这点我们可以通过一张图先来了解一下. prim算法的安全边是从与当前生成树相连接的边中选择 ...
随机推荐
- 微信小程序入门从这里出发(登录注册、开发工具、文件及结构介绍)
(一) 准备工作 (1) 登录注册 注册账号:这就不谈了,只需要注意使用一个全新的邮箱,别之前注册过公众号小程序等就可以了 https://mp.weixin.qq.com/wxopen/waregi ...
- 计算滚动条的宽度--js
原理 创建两个div嵌套在一起 外层的div设置固定宽度和overflow:scroll 滚动条的宽度=外层div的offsetWidth-内层div的offsetWidth 实现代码 /** * 获 ...
- R星游戏如何绑定二次验证码_虚拟MFA_两步验证_谷歌身份验证器?
一般点账户名——设置——安全设置中开通虚拟MFA两步验证 具体步骤见链接 R星游戏如何绑定二次验证码_虚拟MFA_两步验证_谷歌身份验证器? 二次验证码小程序于谷歌身份验证器APP的优势 1.无需下载 ...
- 面试题二十二:链表中倒数第k个节点
方法一:双指针法定义两个指针A.B,A先走k-1步后再一起走,直到A.next==null注意: 1.链表为空 2.链表长度小于k 3.k<=0 当题目是求链表的中间节点时,可以两个指针从开头开 ...
- Java 异常处理专题,从入门到精通
内置异常和Throwable核心方法 Java内置异常 可查异常(必须要在方法里面捕获或者抛出) ClassNoFoundException 应⽤程序试图加载类,找不到对应的类 IllegalAcce ...
- ES Reindex用java来实现
简单的: 核心代码 //发送请求 ReindexRequestBuilder builder=ReindexAction.INSTANCE.newRequestBuilder(client).sour ...
- queue stack for STL
前不久发现自己vector有些不会了,于是想起了queue和stack. 有一个小故事,,,某天我跟自己打赌我queue没有写博园,结果打开一看竟然不知什么时候写过了,而且(QAQ)还有一定的浏览量了 ...
- Day02_IP地址详解&进制转换&DOS基本命令与批处理
学于千峰教育开源课程 感谢 千峰教育官网 b站在线视频 IP地址详解 一.简单局域网的构成 局域网:一般称为内网 简单局域网的构成:交换机.网线.PC(其他IT终端) 交换机:用来组建内网的局域网的设 ...
- math库常用函数
- three.js之初探骨骼动画
今后的几篇郭先生主要说说three.js骨骼动画.three.js骨骼动画十分有意思,但是对于初学者来说,学起来要稍微困难一些,官方文档比较少,网上除了用圆柱体的例子就是引用外部模型的,想要熟练使用骨 ...