图论算法(五)最小生成树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算法的安全边是从与当前生成树相连接的边中选择 ...
随机推荐
- vs code的使用(一) Format On Paste/Format On Save/ Format On Type
很多经典的问题可以搜索出来,但是一些很小的问题网上却没有答案 (这是最令人发狂的,这么简单,网上居然连个相关的信息都没有给出) (就比如我想保存后自动格式化,但网上的大部分都是如何取消保存后自动格式化 ...
- css属性inline-block的应用
1. 让两个块级元素处于同一行 2. 需要元素撑开边框的时候
- 题解 CF938G 【Shortest Path Queries】
题目让我们维护一个连通无向图,边有边权,支持加边删边和询问从\(x\)到\(y\)的异或最短路. 考虑到有删边这样的撤销操作,那么用线段树分治来实现,用线段树来维护询问的时间轴. 将每一条边的出现时间 ...
- ActiveMQ【CVE-2016-3088】上传公钥实现sssh免密登录
Apache-ActiveMQ是apache旗下的消息中间件,至今为止还是有较多的甲方爸爸们,还在使用该中间件.据了解,Apache-ActiveMQ中间件有2个厉害的CVE,一个是CVE-2016- ...
- 修改map中原来的各种Key
简单描述: 做数据迁移的时候,需要展示数据库的字段信息,但是我发现 Oracle的sql查询到的结果 出来默认是大写的 和 前端vue的参数小写开头+驼峰 不太一样 所以后台取到的数据都是是引用Lis ...
- 前端学习(十四):CSS布局
进击のpython ***** 前端学习--CSS布局 每个模块的相关央视就算是进本上都完成了,但是,这些模块想放在不同的位置 横着放,竖着放,斜着放... ... 想怎么放怎么放 那就用到了今天要说 ...
- 聊聊Django应用的部署和性能的那些事儿
随着工作的深入,我越来越发现Python Web开发中有很多坑,也一直在羡慕AspNetCore和Go等的可执行文件部署和高性能,以及Spring生态的丰富,不过因为工作用了Django,生活还是要继 ...
- Mysql 的数据导入导出
一. mysqldump工具基本用法,不适用于大数据备份 1. 备份所有数据库: mysqldump -u root -p --all-databases > all_database_sq ...
- 各版本arm-gcc区别与安装【转】
转自:https://www.jianshu.com/p/fd0103d59d8e arm-linux-gcc.arm-none-eabi-gcc.arm-eabi-gcc.arm-none-linu ...
- PHP move_uploaded_file() 函数
定义和用法 move_uploaded_file() 函数把上传的文件移动到新位置. 如果成功该函数返回 TRUE,如果失败则返回 FALSE. 语法 move_uploaded_file(file, ...