数据结构与算法十一: 4.1 求最小生成树--普利姆算法

216 阅读6分钟

这是我参与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公里
  • 问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短? 修路问题.png

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
    }
}

关注我

如果觉得我写的不错,请点个赞 关注我, 您的支持是我更文最大的动力!