图论篇2——最小生成树算法(kurskal算法&prim算法)
基本概念
树(Tree)
如果一个无向连通图中不存在回路,则这种图称为树。
生成树 (Spanning Tree)
无向连通图G的一个子图如果是一颗包含G的所有顶点的树,则该子图称为G的生成树。
生成树是连通图的极小连通子图。这里所谓极小是指:若在树中任意增加一条边,则将出现一条回路;若去掉一条边,将会使之变成非连通图。
最小生成树
一个带权值的连通图。用$n-1$条边把$n$个顶点连接起来,且连接起来的权值最小。
应用场景
设想有9个村庄,这些村庄构成如下图所示的地理位置,每个村庄的直线距离都不一样。若要在每个村庄间架设网络线缆,若要保证成本最小,则需要选择一条能够联通9个村庄,且长度最小的路线。
Kruskal算法
知识点:数据结构——并查集
基本思想
始终选择当前可用、不会(和已经选取的边)构成回路的最小权植边。
具体步骤:
1. 将所有边按权值进行降序排序
2. 依次选择权值最小的边
3. 若该边的两个顶点落在不同的连通分量上,选择这条边,并把这两个顶点标记为同一连通分量;若这条边的两个顶点落到同一连通分量上,舍弃这条边。反复执行2,3,直到所有的都在同一连通分量上。【这一步需要用到上面的并查集】
模板题:https://www.luogu.org/problem/P3366
#include <iostream>
#include <algorithm>
using namespace std;
int pre[];
int n, m; //n个定点,m条边 struct ENode {
int from, to, dis;
bool operator<(ENode p) {
return dis < p.dis;
}
}M[]; int Find(int x) {
return x == pre[x] ? pre[x] : pre[x] = Find(pre[x]);
} int kurskal() {
sort(M, M + m);
int N = n, res = ;
for (int i = ; i < m && N > ; i++) {
int fx = Find(M[i].from), fy = Find(M[i].to);
if (fx != fy) {
pre[fx] = fy;
N--;//找到了一条边,当N减到1的时候表明已经找到N-1条边了,就完成了
res += M[i].dis;
}
}
if (N == )//循环做完,N不等于1 表明没有找到合适的N-1条边来构成最小生成树
return res;
return -;
} int main() {
cin >> n >> m;
for (int i = ; i <= n; i++) {
pre[i] = i;
}
for (int i = ; i < m; i++) {
scanf("%d%d%d", &M[i].from, &M[i].to, &M[i].dis);;
}
int ans = kurskal();
if (ans != -)
cout << ans << endl;
else
cout << "orz" << endl;
return ;
}
Prim算法
Prim算法思想:
首先将图的点分为两部分,一种是访问过的$u$(第一条边任选),一种是没有访问过的$v$
1: 每次找$u$到$v$的权值最小的边。
2: 然后将这条边中的$v$中的顶点添加到$u$中,直到$v$中边的个数$=$顶点数$-1$
图解步骤:
维护一个$dis$数组,记录只使用已访问节点能够到达各未访问节点最短的权值。
初始值为节点1(任意一个都可以)到各点的值,规定到自己是0,到不了的是$inf$(定义一个特别大的数)。
找当前能到达的权值最短的点。1-->4,节点4
将dis[4]赋值为0,标记为已访问过,同时借助4节点更新dis数组。
后面依次
最后整个dis数组都是0了,最小生成树也就出来了,如果$dis$数组中还有 $inf$ 的话,说明这不是一个连通图。
还是上面那道模板题:https://www.luogu.org/problem/P3366
#include <iostream> #include <fstream>
using namespace std; struct ENode {
int dis, to;//权重、指向
ENode* next = NULL;
void push(int to, int dis) {
ENode* p = new ENode;
p->to = to; p->dis = dis;
p->next = next;
next = p;
}
}*head;
const int inf = << ;
int N, M;
int dis[]; int prim() {
int res = ; for (int i = ; i <= N; i++) {
dis[i] = inf;
} for (int i = ; i < N; i++) {//与kurskal区分,找边是N-1条边,找点是N个点
int v = , MIN = inf;
for (int j = ; j <= N; j++) {
//到不了的,访问过的不进行比较
if (dis[j] != && dis[j] < MIN) {
v = j;
MIN = dis[j];
}
}
if (MIN == inf && v != )//这里v!=1是为了把dis的初始化放在循环里面做,也可以放在循环外面做,但是外层循环就只需要做N-1次了
return -;//还没找够n个点,没路了
res += dis[v];
dis[v] = ;
ENode *p = head[v].next;
while (p) {
if (dis[p->to] > p->dis) {
dis[p->to] = p->dis;
}
p = p->next;
}
}
return res;
} int main() {
#ifdef LOCAL
fstream cin("data.in");
#endif // LOCAL cin >> N >> M;
head = new ENode[N + ];
for (int i = ; i < M; i++) {
int from, to, dis;
scanf("%d%d%d", &from, &to, &dis);
//cin >> from >> to >> dis;
head[from].push(to, dis);
head[to].push(from, dis);
}
int ans = prim();
if (ans != -)
cout << ans << endl;
else
cout << "orz" << endl;
return ;
}
两者区别
时间复杂度
prim算法
时间复杂度为$O(n^2)$,$n$为顶点的数量,其时间复杂度与边得数目无关,适合稠密图。
kruskal算法
时间复杂度为$O(e\cdot loge)$,$e$为边的数目,与顶点数量无关,适合稀疏图。
其实就是排序的时间,因为并查集的查询、合并操作都是$O(1)$。
总结
通俗点说就是,点多边少用Kruskal,因为Kruskal算法每次查找最短的边。 点少边多用Prim,因为它是每次找一个顶点。
具体选择用那个,可以用电脑算一下,题目给的数据级别,$n^2$和$e\cdot loge$看看那个小,比如上面的模板题,题目给的数据级别是$(n<=5000,e<=200000)$,粗略估算一下,kurskal算法一定是会快不少的,结果也确实如粗。
实现难度
明眼人都能看出来,kurskal算法要简单太多了。kurskal算法不需要把图表示出来,而Prim算法必须建表或者邻接矩阵,所以从上面的数据也能看出来当边的数目较大时,Prim算法所占用的空间比kurskal算法多了很多。
拓展
堆优化Prim算法
用堆存储当前所有可到达的点和距离,就是把dis数组里的内容一式两份,存在堆里,然后每次取堆顶元素,每次操作为$O(logn)$,所以使用堆优化后的Prim算法理论上时间复杂度为$O(nlogn)$,但是好像没有达到想要的效果
看了测试数据发现,有很多重边,那就合理了,做了很多次的无用循环,所以时间上也和kurskal比较相近。所以在数据可靠、无重边的情况下,这个算法一定是上述几种中最快的一个。
#include <iostream>
#include <fstream>
#include <cstdio>
#include <queue> using namespace std;
struct P {
int dis, v;
P(int d, int v) :dis(d), v(v) {};
bool operator<(P p)const {
return p.dis < dis;
}
};
struct ENode {
int dis, to;//权重、指向
ENode* next = NULL;
void push(int to, int dis) {
ENode* p = new ENode;
p->to = to; p->dis = dis;
p->next = next;
next = p;
}
}*head;
const int inf = << ;
int N, M;
int dis[];
bool fuck[]; int prim() {
priority_queue<P>pq;
pq.push(P(, ));
int res = , cnt = N;
dis[] = ;
fill(dis + , dis + N + , inf); while (!pq.empty() && cnt > ) {//与kurskal区分,找边是N-1条边,找点是N个点
int v = pq.top().v, d = pq.top().dis;
pq.pop();
if (fuck[v])continue;
fuck[v] = true;
res += d;
cnt--;
ENode* p = head[v].next;
while (p) {
if (dis[p->to] > p->dis) {
dis[p->to] = p->dis;
pq.push(P(p->dis, p->to));
}
p = p->next;
}
}
if (cnt > )
return -;
return res;
} int main() {
#ifdef LOCAL
fstream cin("data.in");
#endif // LOCAL
cin >> N >> M;
head = new ENode[N + ];
for (int i = ; i < M; i++) {
int from, to, dis;
scanf("%d%d%d", &from, &to, &dis);
//cin >> from >> to >> dis;
head[from].push(to, dis);
head[to].push(from, dis);
}
int ans = prim();
if (ans != -)
cout << ans << endl;
else
cout << "orz" << endl;
return ;
}
图论篇2——最小生成树算法(kurskal算法&prim算法)的更多相关文章
- 【数据结构】 最小生成树(三)——prim算法
上一期介绍到了kruskal算法,这个算法诞生于1956年,重难点就是如何判断是否形成回路,此处要用到并查集,不会用当然会觉得难,今天介绍的prim算法在kruskal算法之后一年(即1957年)诞生 ...
- 最小生成树-普利姆(Prim)算法
最小生成树-普利姆(Prim)算法 最小生成树 概念:将给出的所有点连接起来(即从一个点可到任意一个点),且连接路径之和最小的图叫最小生成树.最小生成树属于一种树形结构(树形结构是一种特殊的图),或者 ...
- 一步一步写算法(之prim算法 下)
原文:一步一步写算法(之prim算法 下) [ 声明:版权所有,欢迎转载,请勿用于商业用途. 联系信箱:feixiaoxing @163.com] 前两篇博客我们讨论了prim最小生成树的算法,熟悉 ...
- 一步一步写算法(之prim算法 中)
原文:一步一步写算法(之prim算法 中) [ 声明:版权所有,欢迎转载,请勿用于商业用途. 联系信箱:feixiaoxing @163.com] C)编写最小生成树,涉及创建.挑选和添加过程 MI ...
- 算法起步之Prim算法
原文:算法起步之Prim算法 prim算法是另一种最小生成树算法.他的安全边选择策略跟kruskal略微不同,这点我们可以通过一张图先来了解一下. prim算法的安全边是从与当前生成树相连接的边中选择 ...
- 一步一步写算法(之prim算法 上)
原文:一步一步写算法(之prim算法 上) [ 声明:版权所有,欢迎转载,请勿用于商业用途. 联系信箱:feixiaoxing @163.com] 前面我们讨论了图的创建.添加.删除和保存等问题.今 ...
- 图论---最小生成树----普利姆(Prim)算法
普利姆(Prim)算法 1. 最小生成树(又名:最小权重生成树) 概念:将给出的所有点连接起来(即从一个点可到任意一个点),且连接路径之和最小的图叫最小生成树.最小生成树属于一种树形结构(树形结构是一 ...
- 最小生成树的kruskal、prim算法
kruskal算法和prim算法 都说 kruskal是加边法,prim是加点法 这篇解释也不错:这篇 1.kruskal算法 因为是加边法,所以这个方法比较合适稀疏图.要码这个需要先懂并查集.因为我 ...
- 最小生成树(Minimum Spanning Tree)——Prim算法与Kruskal算法+并查集
最小生成树——Minimum Spanning Tree,是图论中比较重要的模型,通常用于解决实际生活中的路径代价最小一类的问题.我们首先用通俗的语言解释它的定义: 对于有n个节点的有权无向连通图,寻 ...
随机推荐
- [技术博客]ubuntu+nginx+uwsgi+Django+https的部署
ubuntu+nginx+uwsgi+Django+https部署文档 配置机器介绍 操作系统:Ubuntu 18.04.2 LTS 64位 python版本:Python 3.6.7 Django版 ...
- [转帖]Centos7防火墙配置rich-rule实现IP端口限制访问
Centos7防火墙配置rich-rule实现IP端口限制访问 2019-02-18 18:05:35 sunny05296 阅读数 2143 收藏 更多 分类专栏: Linux 版权声明:本文 ...
- influx db
1.查看数据库中的tag keys: 如果需要查看field的直接改 > show tag keys on test; name: garage_pc_overviewtagKey--- ...
- ZYNQ笔记(4):PL触发中断
一.ZYNQ中断框图 PL到PS部分的中断经过ICD控制器分发器后同时进入CPU1 和CPU0.从下面的表格中可以看到中断向量的具体值.PL到PS部分一共有20个中断可以使用.其中4个是快速中断.剩余 ...
- vip视频会员共享电影免费看
vip会员在线看是一款可以观看持有会员特权的视频才能看的网站程序,小而强大,目前支持所有视频网站解析! 解析需要时间耐心等待20秒,如果加载失败,请切换接口耐心等待20秒! 找要要看的vip电影地址, ...
- 【题解】Luogu P4284 [SHOI2014]概率充电器
原题传送门 我们知道,每个电器充电对充电电器数的贡献都是相等的1,所以若第\(i\)个电器有\(p_i\)的概率充电时 \[E=\sum_{i=1}^np_i\] 我们考虑如何求\(p_i\),根据树 ...
- BZOJ3277 串(后缀自动机)
对多串建立SAM的一种方法是加分隔符.于是加完分隔符建出SAM. 考虑统计出每个节点被多少个串包含.让每个串各自在SAM上跑,跑到一个节点就标记(显然一定会完全匹配该节点,因为是对包含其的串建的SAM ...
- golang --rune
rune 是int32的别名类型,专用于存储Unicode编码的单个字符 我们可以用5种方式来表示一个rune字面量: 该rune字面量所对应的字符,如'a'必须是Unicode编码规范所支持的 使用 ...
- Flink基本的API
Flink使用 DataSet 和 DataStream 代表数据集.DateSet 用于批处理,代表数据是有限的:而 DataStream 用于流数据,代表数据是无界的.数据集中的数据是不可以变的, ...
- cygwin中修改path变量
1.在家目录建立 .bash_profile 文件. 2.在该文件添加: export PATH=/my/path/:$PATH 3.解释,/my/path/为你要添加的目录,为什么不在.bashrc ...