最小生成树,Prim和Kruskal的原理与实现
文章首先于微信公众号:小K算法,关注第一时间获取更新信息
1 新农村建设
大清都亡了,我们村还没有通网。为了响应国家的新农村建设的号召,村里也开始了网络工程的建设。
穷乡僻壤,人烟稀少,如何布局网线,成了当下村委会首个急需攻克的难题。
如下图,农户之间的距离随机,建设网线的成本与距离成正比,怎样才能用最少的成本将整个村的农户网络连通呢?
2 思考
如果农户A到农户B,农户B到农户C的网线已经建好了,那农户A和农户C也间接的连通了,不用再建设。
每一根线都可以连通2个农户,所以有N个农户,就只需要N-1条网线就可以了。
3 问题建模
将上述问题转化为无向图来表示。
用邻接矩阵存储农户之间的距离。
这样问题就转化成:找N-1条边将上述图组成一个连通图,要求N-1条边的权值和最小。
这就是经典的最小生成树问题。有两种算法专门解决这类问题,Prim和Kruskal。
4 Prim
4.1 反向思考
对于一个N个点,N-1条边的连通图:
如果剪掉1条线,整个图会变成2个连通子图;如果剪掉2条线,就会变成3个连通子图。
如果剪掉了B到D之间的网线,这时变成2个连通子图。
- 连通子图1:A,B
- 连通子图2:C,D,E,F
为了将整个图连通,就需要找出两个子图之间的最小距离边,连通这条边就行了。
其实就是找出子图1中的所有点到子图2中的所有点的最小边。
这里有3条边,A-C,B-D,B-E,其中A-C距离最小,连通这条边就是最好的方案。
推论:
- 最优的方案是确定的,即最小权值和唯一
- 在最优方案中,剪掉任意一条边所分隔出的两个连通子图,之间的最小距离都应该是剪掉的这条,没有比这条边更小的,否则可以换掉这条边构成新的最优方案
如上图就不是最优方案,因为两个子图之间还有更小的边
4.2 Prim算法框架
对于加权连通图G=(V,E),V为顶点集,E为边集。
- 以V中任意一点x为起点,将x加入一个新的顶点集S={x},初始新的边集T={}为空
- 重复如下步骤直到S=V:
1)选择E中最小的边<u,v>,其中u属于S,而v不属于S但属于V
2)将v加入S,将边<u,v>加入T - 最终S,T即为所求最小生成树
算法解释:把S和非S想象成两个子图,每一步其实就是在找出这两个子图之间的最小边。
过程模拟如下图:
- 以A为起点,将A加入S={A};
- 第一条最小边为A-B,将B加入S={A,B}
- 第二条最小边为B-D,将D加入S={A,B,D}
- 第三条最小边为D-C,将C加入S={A,B,D,C}
继续重复以上过程直到S=V,T即为所求边集。
4.3 Prim代码实现
变量定义
const int MAXN = 100;
int n, m, temp, ans = 0, map[MAXN][MAXN], length[MAXN];
char s, t;
bool flag[MAXN];
初始化
void init() {
cin >> n >> m;
memset(map, -1, MAXN * MAXN);
for (int i = 0; i < n; ++i) {
map[i][i] = 0;
flag[i] = false;
length[i] = 0x7fffffff;
}
for (int i = 0; i < m; ++i) {
cin >> s >> t >> temp;
map[s - 'A'][t - 'A'] = temp;
map[t - 'A'][s - 'A'] = temp;
}
}
核心算法
int main() {
init();
// 将0作为起点加入集合S
flag[0] = true;
for (int i = 0; i < n; ++i) {
if (map[0][i] >= 0) {
length[i] = map[0][i];
}
}
// 选择N-1条边
for (int i = 0; i < n - 1; ++i) {
int min = 0x7fffffff;
int k = 0;
// 枚举非S所有点,选择最小的边
for (int j = 1; j < n; ++j) {
if (!flag[j] && length[j] < min) {
min = length[j];
k = j;
}
}
ans += min;
cout << "k=" << k << " ,min=" << min << endl;
// 将新的点k加入集合S,并通过k更新非S中点的距离
flag[k] = true;
for (int j = 1; j < n; ++j) {
if (!flag[j] && map[k][j] >= 0 && map[k][j] < length[j]) {
length[j] = map[k][j];
}
}
}
cout << "ans=" << ans;
return 0;
}
5 Kruskal
5.1 思考
最优解是要选取N-1条边,边的数量是固定的,但边的权值不一样,所以可以让这N-1条边尽可能的小。那就可以用贪心的思想,从最小的边开始选择。
如上图,从最小的边开始选择,第1条是A-B,第2条是B-D,第3条是A-D。
但这里就出现了冲突,因为A与D已经连通,再多一条边会形成环,没有意义。
所以再多加一个判断,如果一条边所关联的两个点已经连通就不能选择,否则可以选择。
当选择第4条边D-E时,判断D和E没有连通,将这两个子图连通。把两个子图看成不同的集合,这一步就是合并成同一个集合。
如果初始每个点都属于一个独立的集合,每选择一条边,就将所在的集合合并成同一个,在下一次选择边的时候,就只需判断关联的两个点是否为同一集合。这就可以用并查集快速处理。
详细可查看并查集专题。
5.2 Kruskal算法框架
对于加权连通图G=(V,E),V为顶点集,E为边集。
- 初始一个非连通图T=(V,{}),即含所有点,边集为空
- 重复以下步骤,直到成功选择N-1条边
1)在E中取出最小边<u,v>,如果u,v没有连通,就将该边加入T,同时将u,v连通;否则舍弃判断下一条最小边。 - 最终T即为所求最小生成树
过程模拟如下图:
- 判断第1条边B-D,将B,D合并为一个集合;判断第2条边A-B,将A,B,D合并为一个集合
- 判断第3条边A-D,A,D已经属于同一个集合,放弃选择
- 判断第4条边E-F,将E,F合并为一个集合
继续重复以上过程直到选出N-1条边。
5.3 Kruskal代码实现
变量定义
struct Edge {
int start;
int end;
int value;
};
const int MAXN = 100, MAXM = 100;
int n, m, answer = 0, edgeNum = 0, father[MAXN];
Edge edge[MAXM];
初始化
void init() {
char s, e;
int temp;
// 并查集根结点,初始为-1,合并之后为-num,num表示集合中的个数
memset(father, -1, MAXN);
cin >> n >> m;
for (int i = 0; i < m; i++) {
cin >> s >> e >> temp;
edge[i].start = s - 'A';
edge[i].end = e - 'A';
edge[i].value = temp;
}
}
bool compare(const Edge &a, const Edge &b) {
return a.value < b.value;
}
查找根
int findFather(int s) {
int root = s, temp;
// 查找s的最顶层根
while (father[root] >= 0) {
root = father[root];
}
// 路径压缩,提高后续查找效率
while (s != root) {
temp = father[s];
father[s] = root;
s = temp;
}
return root;
}
合并集合
void unionSet(int s, int e) {
int rootS = findFather(s);
int rootE = findFather(e);
int weight = father[rootS] + father[rootE];
// 将结点数少的集合作为结点数多的集合的儿子节点
if (father[rootS] > father[rootE]) {
father[rootS] = rootE;
father[rootE] = weight;
} else {
father[rootE] = rootS;
father[rootS] = weight;
}
}
核心算法
int main() {
init();
sort(edge, edge + m, compare);
for (int i = 0; i < m; i++) {
if (findFather(edge[i].start) != findFather(edge[i].end)) {
unionSet(edge[i].start, edge[i].end);
answer += edge[i].value;
edgeNum++;
if (edgeNum == n - 1) {
break;
}
}
}
cout << answer << endl;
return 0;
}
6 总结
prim基于顶点操作,适用于点少边多的场景,多用邻接矩阵存储。
kruskal基于边操作,适用于边少点多的场景,多用邻接表存储。
扫描下方二维码关注公众号,第一时间获取更新信息!
最小生成树,Prim和Kruskal的原理与实现的更多相关文章
- poj1861 最小生成树 prim & kruskal
// poj1861 最小生成树 prim & kruskal // // 一个水题,为的仅仅是回味一下模板.日后好有个照顾不是 #include <cstdio> #includ ...
- 图的最小生成树(Prim、Kruskal)
理论: Prim: 基本思想:假设G=(V,E)是连通的,TE是G上最小生成树中边的集合.算法从U={u0}(u0∈V).TE={}开始.重复执行下列操作: 在所有u∈U,v∈V-U的边(u,v)∈E ...
- 最小生成树 Prim算法 Kruskal算法实现
最小生成树定义 最小生成树是一副连通加权无向图中一棵权值最小的生成树. 在一给定的无向图 G = (V, E) 中,(u, v) 代表连接顶点 u 与顶点 v 的边(即,而 w(u, v) 代表此边的 ...
- 最小生成树Prim算法 Kruskal算法
Prim算法(贪心策略)N^2 选定图中任意定点v0,从v0开始生成最小生成树 树中节点Va,树外节点Vb 最开始选一个点为Va,其余Vb, 之后不断加Vb到Va最短距离的点 1.初始化d[v0]=0 ...
- 最小生成树--Prim及Kruskal
//prim算法#include<cstdio> #include<cmath> #include<cstring> #include<iostream> ...
- 最小生成树prim和kruskal模板
prim: int cost[MAX_V][MAX_V]; //cost[u][v]表示边e=(u,v)的权值(不存在的情况下设为INF) int mincost[MAX_V]; //从集合X出发的每 ...
- 最小生成树Prim算法Kruskal算法
Prim算法采用与Dijkstra.Bellamn-Ford算法一样的“蓝白点”思想:白点代表已经进入最小生成树的点,蓝点代表未进入最小生成树的点. 算法分析 & 思想讲解: Prim算法每次 ...
- 最小生成树 Prim和Kruskal
感觉挺简单的,Prim和Dijkstra差不多,Kruskal搞个并查集就行了,直接上代码吧,核心思路都是找最小的边. Prim int n,m; int g[N][N]; int u,v; int ...
- 邻接矩阵c源码(构造邻接矩阵,深度优先遍历,广度优先遍历,最小生成树prim,kruskal算法)
matrix.c #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include < ...
随机推荐
- queueMicrotask & EventLoop & macrotask & microtask
queueMicrotask https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/queueMicro ...
- js 监听ajax请求
function hookSend(hook) { if (!XMLHttpRequest.prototype._oldSend) XMLHttpRequest.prototype._oldSend ...
- NGK每日快讯2021.1.29日NGK公链第87期官方快讯!
- 怎么创建CSV文件和怎么打开CSV文件
CSV(Comma Separated Values逗号分隔值). .csv是一种文件格式(如.txt..doc等),也可理解.csv文件就是一种特殊格式的纯文本文件.即是一组字符序列,字符之间已英文 ...
- 聊聊CacheLine
本文转载自聊聊CacheLine 导语 文章聊聊缓存一致性协议中我们提到过,缓存里面最小的单位是缓存行/缓存条目,但是缓存中的具体存储结构是什么样的,缓存行中有存放的是什么?在缓存中是如何寻找指定是还 ...
- 疯狂的String
本文转载自疯狂的String 导语 在java中字符串是我们比较常用的一个类型,字符串是不可变的,类被声明为final , 存储字符的char[] value数据也被声明为final ,我们对Stri ...
- Java NIO wakeup实现原理
本文转载自Java NIO wakeup实现原理 导语 最近在阅读netty源码时,很好奇Java NIO中Selector的wakeup()方法是如何唤醒selector的,于是决定深扒一下wake ...
- Iterative learning control for linear discrete delay systems via discrete matrix delayed exponential function approach
对于一类具有随机变迭代长度的问题,如功能性电刺激,用户可以提前结束实验过程,论文也是将离散矩阵延迟指数函数引入到状态方程中. 论文中关于迭代长度有三个定义值:\(Z^Ta\) 为最小的实验长度,\(Z ...
- C语言柔性数组和动态数组
[前言]经常看到C语言里的两个数组,总结一下. 一.柔性数组 参考:https://www.cnblogs.com/veis/p/7073076.html #include<stdio.h> ...
- Hive 填坑指南
Hive 填坑指南 目录 Hive 填坑指南 数据表备份 数据表备份 方法1:create table 表名_new as select * from 原表 create table 表名_new a ...