“这是我参与8月更文挑战的第27天,活动详情查看:8月更文挑战”
关注我,以下内容持续更新
1.普利姆算法介绍
普利姆(Prim)算法的本质是求最小生成树,可以解决类似于修路问题和公交站问题等问题。普利姆(Prim)算法求最小生成树,最小生成树(Minimum Cost Spanning Tree),简称MST。也就是在包含n个顶点的连通图中,找出只有(n-1)条边包含所有n个顶点的连通子图,使所有边上的权的总和最小,也就是所谓的极小连通子图;求最小生成树的算法主要是普里姆算法和克鲁斯卡尔算法。
2. 应用场景:修路问题(本质是求最小生成树)
- 胜利乡有7个村庄(A,B,C,D,E,F,G),现在需要修路把7个村庄连通
- 各个村庄的距离用边线表示(权),比如A-B距离5公里
- 问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?
3. 普利姆的算法思路
- ① 设G=(V,E)是连通网;T=(U,D)是最小生成树;V是G中的顶点集合,U是T中的顶点集合;E是G中的边的集合,D是T中的边的集合;
- ② 若从顶点u开始构造最小生成树,则从集合V中取出顶点u放入集合U中,标记顶点u的visited[u]=1;
- ③ 若集合U中顶点ui与集合V中的顶点vj之间存在边,则寻找这些边中权值最小的边,但不能构成回路,将顶点vj加入集合U中,将边(ui,vj)加入集合D中,标记visited[vj]=1
- 重复步骤2,直到U与V相等,即所有顶点都被标记为访问过,此时D中有n-1条边
4. 图解普利姆算法完整过程
① 假设从顶点A开始处理,那么最小生成树的顶点集合U中有<A>;
② 从<A>开始处理,广度优先搜索 A的下一层邻近顶点有 C,G,B,所以A可以访问的顶点有 A-C(7),A-G(2),A-B(5),其中 A-G 最小,于是把G加入U中,此时U中有<A,G>;
③ 从<A,G>开始处理,将A和G和他们相邻的未访问的结点进行处理,有A-C(7),A-B(5),G-B(3),G-E(4),G-F(6),其中G-B(3)最小,于是把B加入U,此时U中为<A,G,B>;
④ 从<A,G,B>开始处理,将A,G,B和他们相邻的未访问的结点进行处理,有A-C(7),G-E(4),G-F(6),B-D(9),其中 G-E 最小,于是把E加入U中,此时 U中为<A,G,B,E>;
⑤ 从<A,G,B,E>开始处理,将A,G,B,E和他们相邻的未访问的结点进行处理,有A-C(7),G-F(6),B-D(9),C-E(8),E-F(5),其中E-F(5)最小,于是把F加入U中,此时U中为<A,G,B,E,F>;
⑥ 从<A,G,B,E,F>开始处理,将A,G,B,E,F和他们相邻的未访问的结点进行处理,有A-C(7),B-D(9),C-E(8),F-D(4),其中F-D(4)最小,于是把D加入U,此时U中为<A,G,B,E,F,D>;
⑦ 从<A,G,B,E,F,D>开始处理,将A,G,B,E,F,D和他们相邻的未访问的结点进行处理,有A-C(7),C-E(8),其中A-C(7)最小,于是把C加入U中,此时U中为<A,G,B,E,F,D,C>;(以上每一步都会判断U中的顶点个数和V中顶点个数是否相等)当走到第七步时,U的顶点个数等于V的顶点个数,此时结束.
5. 代码实现
-(void)primCase{
NSMutableArray<NSString*>* data = [@[@"A",@"B",@"C",@"D",@"E",@"F",@"G"] mutableCopy];
NSInteger verxs = data.count;
//因为要不断读取两个顶点之间边的权值,所以使用邻接矩阵weight存储权值效率高
//用10000表示无法直接连通
#define N 10000
NSMutableArray<NSMutableArray<NSNumber*>*>*weight = [@[
@[@(N),@(5),@(7),@(N),@(N),@(N),@(2)],
@[@(5),@(N),@(N),@(9),@(N),@(N),@(3)],
@[@(7),@(N),@(N),@(N),@(8),@(N),@(N)],
@[@(N),@(9),@(N),@(N),@(N),@(4),@(N)],
@[@(N),@(N),@(8),@(N),@(N),@(5),@(4)],
@[@(N),@(N),@(N),@(4),@(5),@(N),@(6)],
@[@(2),@(3),@(N),@(N),@(4),@(6),@(N)]] mutableCopy];
Graph*graph = [[Graph alloc]initWithVertex:verxs data:data weight:weight];
[graph showGraph];//打印图看看效果
[self prim:graph vIndex:0];//这是老师的思路
//[self primMyself:graph vIndex:0];//这是我自己实现的思路,更容易理解
}
/**
代码思路:
一共三层循环:假设有 7 个顶点,最外层循环的作用是让内部循环执行7次,第二层内循环是seletes已生成最小生成树中的顶点,遍历seletes;第三层循环挨个比较所有顶点与选出seletes[i]形成的边,取出最小值加入seletes;最外层循环执行完成后,seletes已有 7 个顶点最小生成树已生成
*/
/**
* 最后输出seletes为A,G,B,E,F,D,C,验证通过
* **@param** graph 图
* **@param** vIndex 表示从图的第几个顶点开始生成'A':0 'B':1,...
*/
-(void)prim:(Graph*)graph v:(NSInteger)v{
//seletes存放现有生成树的顶点;每次取出seletes中的点,遍历下一层邻近顶点,添加这些邻近顶点中最小的边
NSMutableArray*seletes = [[NSMutableArray alloc]initWithCapacity:graph.vertex];
//visited[] 标记结点(顶点)是否被访问过
NSMutableArray*visited = [[NSMutableArray alloc]initWithCapacity:graph.vertex];
for (int i = 0; i<graph.vertex; i++) {
visited[i] = @(0);
}
visited[v] = @(1);//当前节点标记为已访问
[seletesU addObject:graph.data[v]];//先把初始节点加入U中;
int minWeight = 10000;//将 minWeight 初始成一个大数,后面在遍历过程中,会被替换
int tmpi = 0,tmpj = 0;
//因为有 graph.verxs顶点,普利姆算法结束后,有 graph.verxs-1边
// 1. 因为有 7 个顶点,所以循环 7 次.最外层循环的作用就是让内部2个循环执行7次
for (int k = 1; k<graph.vertexCount; k++) {
//2.遍历seletes: [visited[i] intValue] == 1代表seletes中的点
for (int i = 0; i<graph.vertexCount; i++) {
//3.广度优先搜索遍历下一层邻近顶点,选出最小的边,加入seletes
for (int j = 0; j<graph.vertexCount; j++) {
//4.选出最小的边,加入seletes
if ([visited[i] intValue] == 1 && [visited[j] intValue] == 0 && [graph.matrix[i][j] intValue]<minWeight) {
tmpi = i;
tmpj = j;
minWeight = [graph.matrix[i][j] intValue];
}
}
}
//5.把最小的边,加入seletes
visited[tmpj] = @(1);//标记为已访问
[seletes addObject:graph.data[tmpj]];
printf("第%d轮处理:\n",k);
printf("%s加入最小生成树的节点集合;\n",graph.data[tmpj].UTF8String);
printf("边<%s,%s> 权值:(%d)加入最小生成树;\n",graph.data[tmpi].UTF8String,graph.data[tmpj].UTF8String,minWeight);
printf("此时最小生成树中的顶点%s\n\n",seletes.description.UTF8String);
minWeight = 10000;//注意:每一次添加完成后一定要重置minWeight
}
}
//Graph.h 文件
@interface Graph : NSObject
@property(assign,nonatomic)NSInteger vertexCount;//表示图的节点总数
@property(strong,nonatomic)NSMutableArray<NSString*>* vertexs;//表示图的节点数组
@property(strong,nonatomic)NSMutableArray<NSMutableArray<NSNumber*>*>* matrix;//用邻接矩阵存放图的各边的权重
- (instancetype)initWithVertexs:(NSMutableArray *)vertexs matrix:(NSMutableArray<NSMutableArray<NSNumber *> *> *)matrix;
-(void)showGraph;
@end
// Graph.m 文件
#import "Graph.h"
@implementation Graph
- (instancetype)initWithVertexs:(NSMutableArray *)vertexs matrix:(NSMutableArray<NSMutableArray<NSNumber *> *> *)matrix{
self = [super init];
if (self) {
self.vertexCount = vertexs.count;
self.vertexs = vertexs;
self.matrix = matrix;
}
return self;
}
-(void)showGraph{
for (int i = 0; i<self.matrix.count; i++) {
for (int j = 0; j<self.matrix[i].count; j++) {
if ([self.matrix[i][j] intValue] == 10000) {
printf(" N ");
}else{
printf("%2d ",[self.matrix[i][j] intValue]);
}
}
printf("\n");
}
}
@end
普利姆算法核心代码的第二种实现
注:这是普利姆算法核心代码的另一种实现思路,与第一种很相似,如果第一种理解困难的话,这一种或许会更好理解。
/**
代码思路:
一共三层循环:假设有 7 个顶点,最外层循环的作用是让内部循环执行 7 次,第二层内循环是seletes已生成最小生成树中的顶点,遍历seletes;第三层循环挨个比较所有顶点与选出seletes[i]形成的边,取出最小值加入seletes;最外层循环执行完成后,seletes已有 7 个顶点最小生成树已生成
*/
-(void)primMyself:(Graph*)graph vIndex:(NSInteger)vIndex{
//seletes存放现有生成树的顶点;每次取出seletes中的点,遍历下一层邻近顶点,添加这些邻近顶点中最小的边
NSMutableArray<NSNumber*>*seletes = [[NSMutableArray alloc]initWithCapacity:graph.vertexCount];
//visited[] 标记结点(顶点)是否被访问过
NSMutableArray*visited = [[NSMutableArray alloc]initWithCapacity:graph.vertexCount];
for (int i = 0; i<graph.vertexCount; i++) {
visited[i] = @(0);
}
visited[vIndex] = @(1);//当前节点标记为已访问
[seletes addObject:@(vIndex)];//先把初始节点加入U中;
int minWeight = 10000;//将 minWeight 初始成一个大数,后面在遍历过程中,会被替换
int tmpi = 0,tmpj = 0;
//因为有 graph.verxs顶点,普利姆算法结束后,有 graph.verxs-1边
//1. 因为有 7 个顶点,所以循环 7 次.最外层循环的作用就是让内部2个循环执行7次
for (int k = 1; k<graph.vertexCount; k++) {
//2.遍历seletes
for (int i = 0; i<seletes.count; i++) {
int index = [seletes[i] intValue];
//3.广度优先搜索遍历index的下一层邻近顶点,选出最小的边,加入seletes
for (int j = 0; j<graph.vertexCount; j++) {
//4. 选出最小的边,加入seletes
if ([visited[j] intValue] == 0 && [graph.matrix[index][j] intValue]<minWeight) {
tmpi = index;
tmpj = j;
minWeight = [graph.matrix[index][j] intValue];
}
}
}
//5.把最小的边,加入seletes
visited[tmpj] = @(1);//标记为已访问
[seletes addObject:@(tmpj)];
printf("第%d轮处理:\n",k);
printf("%s加入最小生成树的节点集合;\n",graph.vertexs[tmpj].UTF8String);
printf("边<%s,%s>(%d)加入最小生成树;\n",graph.vertexs[tmpi].UTF8String,graph.vertexs[tmpj].UTF8String,minWeight);
printf("此时最小生成树中的顶点%s\n\n",seletes.description.UTF8String);
minWeight = 10000;//注意:每一次添加完成后一定要重置minWeight
}
}
关注我
如果觉得我写的不错,请点个赞 关注我, 您的支持是我更文最大的动力!