最小生成树算法之Prim算法

历时两个下午,终于完成了这个效率并不高的Prim算法。第一个下午想出了基本的处理逻辑并实现了,但是每次执行都报segmentfault 11错误,也即是内存被耗光了吧。因为在MAC的终端通过cc 编译运行,没有使用lldb进行调试,所以短一点的代码,凭借肉眼观察,一遍又一遍的过滤代码还是能够找到自己犯傻的地方的,但是行数一多就跪了。

所以今天下午的主要任务是在Xcode上断点运行,一步一步测验到底在哪里出错了!

首先上代码:

Prim最小生成树算法:

//  Created by Wang Bing on 16/9/2.
//  Copyright © 2016年 Wang Bing. All rights reserved.
//
//使用邻接矩阵存储
#include<stdio.h>
#include<stdlib.h>
#define MAX_VERTEX_NUM 20
#define INF 10000 // 表示顶点之间不可达
#define FALSE 0
#define TRUE 1
typedef int bool;

typedef struct ArcCell //若info用不到,可以直接定义AdjMatrix[20][20],这里用最难的方式实现
{
    int adj;//存储边之间的权值
    // int info; //这里用不到
} ArcCell,AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
// 定义图
typedef struct
{
    int vexs[MAX_VERTEX_NUM];//顶点向量,存储顶点内容
    AdjMatrix arcs;//存储顶点之间的关系
    int vexnum, arcnum;//图的顶点数和边数
}MGraph;

int LocateElem(MGraph *G, int c)
{
    for(int i = 0; i < G->vexnum;i++)
    {
        if(G->vexs[i] == c)
            return i;
    }
    return MAX_VERTEX_NUM; //返回一个矩阵中不用的下标数字即可
}

void CreateGraph(MGraph *G)//构造图
{
    printf("请输入顶点数,边数:\n");
    scanf("%d%d",&G->vexnum,&G->arcnum);//输入图的顶点数目和边数目

    // printf("请输入顶点内容,顶点数为%d\n",G->vexnum);

    for(int i = 0; i < G->vexnum; i++)//初始化图的顶点
    {
        // printf("输入结点%d: ",i);
        scanf("%d",&G->vexs[i]); //输入顶点信息:a,b,c,d...,假设顶点值各不相同
        // getchar();
    }

    printf("结点内容输入完毕!---\n");
    //初始化邻接矩阵
    for(int i = 0; i < G->vexnum; i++)
    {
        for(int j = 0; j < G->vexnum; j++)
        {
            G->arcs[i][j].adj = INF;//设置顶点间的距离都是INF
        }
    }

    //构造邻接矩阵
    printf("请输入边:\n");
    for(int k = 0; k < G->arcnum; k++)
    {
        // printf("请输入边:\n");
        int u,v;//u,v分别为顶点存储内容
        int w; //w是边的权值
        scanf("%d %d %d",&u,&v,&w); //输入边
        // getchar();
        //根据顶点内容找到结点的下标--在一维数组中寻找
        int i = LocateElem(G,u);
        int j = LocateElem(G,v);
        G->arcs[i][j].adj = w;
        G->arcs[j][i].adj = w; //无向,所以对称边也赋值
    }
}

MGraph* Prim(MGraph *MG)
{
    MGraph *G = (MGraph*)malloc(sizeof(MGraph));
    if(G == NULL)
    {
        printf("ERROR\n");
    }
    G->vexnum = MG->vexnum;
    G->arcnum = 0;
    //清洗掉权值数据,赋值为INF
    for(int i = 0; i < G->vexnum; i++)
    {
        for(int j = 0; j < G->vexnum; j++)
        {
            G->arcs[i][j].adj = INF;//设置顶点间的距离都是INF
            G->arcs[j][i].adj = INF;//逆向清洗连接为INF
        }
    }
    //顶点的值赋予G
    for(int i = 0; i < MG->vexnum;i++)
    {
        G->vexs[i] = MG->vexs[i];
    }

    //设定一个标记数组,标记结点是否在树中
    bool NodeInTree[MAX_VERTEX_NUM];

    int count;//记录已经加入到树中的结点数目
    for(int i = 0; i < MAX_VERTEX_NUM; i++)
    {
        NodeInTree[i] = FALSE; //初始时没有一个结点在树中
    }

    //任意选择一个结点
    int u,v;
    u = G->vexnum / 2; //从中间开始进行构造树
    v = MAX_VERTEX_NUM;
    NodeInTree[u] = TRUE; //标记结点选中放到树中
    count = 1;

    while(count < G->vexnum) //直到加入树中的结点数与G.vexnum相等为止
    {

        //寻找U,V-U之间最小边,两层for循环
        int i = 0,j = 0;
        int MinEdge = INF;//初始化
        //while循环和for循环都是在满足的情况下遍历!!不然直接就跳过去了
        for(i = 0; i < G->vexnum; i++)
        {
            for(j = 0; j < G->vexnum; j++)
            {
                if((NodeInTree[i] == TRUE) && (NodeInTree[j] == FALSE))//i记作在树中的结点,j记作不在树中的结点
                {
                    if(MinEdge > MG->arcs[i][j].adj)
                    {
                        MinEdge = MG->arcs[i][j].adj;
                        u = i;
                        v = j;
                    }
                }
            }
        }
        G->arcs[u][v].adj = G->arcs[v][u].adj = MG->arcs[u][v].adj; //边加入树中
        G->arcnum++;//成功加入一条边
        NodeInTree[v] = TRUE;//结点加入树中
        count++;//又多了一个结点

    }
    free(MG);
    return G;
}

int main(void)
{
    MGraph *G = (MGraph*)malloc(sizeof(MGraph));
    CreateGraph(G);
    printf("%d %d\n", G->vexnum,G->arcnum);

    G = Prim(G);

    for(int i = 0; i < G->vexnum; i++)
    {
        for(int j = 0; j < G->vexnum; j++)
        {
            printf("%d ", G->arcs[i][j].adj);
        }
        printf("\n");
    }
    return 0;
}

本来这次学习Prim算法是想找别人的代码看看,写几遍记住,再加一点思考,然后就说服自己我会了这个算法的。但是当我复习完最小生成树的基本概念与通用算法的步骤后,我想既然手动能够模拟这个过程,为什么不能自己动手实现?毕竟了解一个青蛙最好的方式是造一个青蛙。

但是,自己造,有一点恐惧。

可是,前几天听到某罗总结什么是学习,学习本身就是一个让人有些许痛苦的过程,是在把新东西缝到自己的知识体系,学习必须要走出舒适区!

好吧,我承认自己以前学习算法都很偷懒,就看人家实现的完整版。毫无疑问,通过这次的实践,我体会到,可能几行简单的代码,你读起来觉得,啊,这理所应当啊,要是我自己写肯定也是这么做的,于是,就跨过去了。

然而,你不知道的是,作者在实现这个代码的时候他经历了怎样的挣扎,当他还不是很熟悉,需要一步一步想明白的时候。

那么,请你回到上面瞄一眼我写的代码,你能猜出哪些部分让我陷入泥沼,费了很大的力气才get out的吗?

我明白了,如果我在博客上看到这样一个我需要的算法,我直接拿过去,咀嚼这个人的思路,当然也会有一点点难受,像是在舒适区之外,但毕竟不是自己在创造,而只是在结合自己的经验,知识储备,理解一个东西而已,这本质上是完全不同的事情!

因此,我放出这段代码,主要目的不是供大家拿过去用,这个实现本身并不漂亮,循环都套了三层了!

我的主要目的是,分享自己的心得,像我这样一个并不聪明的人,如何通过思考,提高自己的认知。

ps:人的智商有高有低,但总体是正态分布,差别只是在最高的20%和最低的20%。但是人的认知是幂律分布,可能某人的认知水平,只是储备是你的指数倍!认知又不是主要靠智商,对吧。靠什么,我想大家都懂了~

那么,要写出这样一个算法,你需要掌握哪些基础知识就能够自己尝试写出来呢?

假设你对C还是蛮熟悉的,部分重点,我踩的坑会再提出来说明。

首先,你需要知道如何构建图,虽然在纸上,用笔画出几个圈,再往圈上连几根线就成了图,但是往计算机里存储这个图是基本功!

你看上面很大一部分代码都是在构建图对吧!

关于图的构建

我们知道,在计算机的存储中,我们可以用两种方式去存图,第一种是邻接矩阵法,第二种是邻接表法。这里就代码论代码,因此只讨论邻接矩阵法。

用的是严版的实现。我喜欢严奶奶的数据结构!第一遍看不懂,第二遍懵懂,第三遍,惊叹。
不要问我读了几遍。。
多少次想把这本书扔了。

OK,首先还是把头部拿过来,不然比较容易懵:

#include<stdio.h>
#include<stdlib.h>
#define MAX_VERTEX_NUM 20//假设我们最大只存20*20个结点好了
#define INF 10000 // 表示顶点之间不可达,定义这个英菲尼迪啊(很大很大的意思)
#define FALSE 0
#define TRUE 1
typedef int bool;

因为C中没有原版的布尔类型(要是说错了,别打我),听了几种引入头文件的方式,在本机上测试都失败,想想算了还不如自己搞对吧。因此定义了 FALSE = 0, TRUE = 1, bool = int型。
反正C中if(0) 就表示条件为假,if(!0)表示真的对吧。

既然是邻接矩阵,那么我们知道矩阵里面存储的是边的关系,若是图是无权的,那么用0,1就可以表示两个顶点之间是否有边了,要是图是有权图,就需要存储这个权值。要是两点之间不可达,我们用的不是0而是无穷大(INF),用0肯定不合适,人家小数字表示很近,你0岂不是表达更近?但其实是不可达。那就尴尬了。

所以矩阵格子中存储一个数字就可以了。但是我们的严奶奶说,用一个ArcCell结构体来夹带这个数字,还能夹带点其他的东西,比如这条边有什么好玩的性质啊,存在结构体里面,任你想象。

我们的眼光要穿越时光,看到将来的需求。

typedef struct ArcCell //若info用不到,可以直接定义AdjMatrix[20][20],这里用最难的方式实现
{
    int adj;//存储边之间的权值
    // int info; //这里用不到
} ArcCell,AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];

我相信很多人看到这种结构体定义,脑子就嗡的一下,然后硬着头皮看。但事实上不必这么为难。

首先

typedef struct name1
{
    //some stuff
} name2;

问你,name1和name2有什么区别呢?
这么回答,name1是结构体的名字,可以用这个结构体再去定义新的类型。name2呢,是它生成第一个儿子,是一个数据类型了!就像int, double等等,可以拿过去定义数据了。

也即:

结构体的名字用来定义数据类型。
结构体定义的类型用来定义数据。

我们可以用
struct name1* n;
定义了一个 指向name1型的指针。

name2就不能拿过来定义数据类型,但是可以定义数据。比如这样:
name1 *a = (n)malloc(sizeof(a));

所以常常见到人们把这两个名字搞成一样的,这样就不用管他们的区别了,不得不说,真机智。。

所以看到结构体后面跟着的
ArcCell,AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM],
ArcCell好理解,一个类型的名字。
那么AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM]呢?
是定义了很多个类型集合吗?

绝不是啊,思考一下,我们定义一个数据容器,数据容器自己能装数据类型吗?拿具体的东西只能装具体的东西,数据类型是一种抽象。

所以这个定义是:定义了一个存储ArcCell这种数据类型的数据容器,这个数据容器是个二维数组。

这种写法是较为省略的,也可以用两步写法。
1.定义了ArcCell
2. typedef ArcCel[MAX_VERTEX_NUM][MAX_VERTEX_NUM] AdjMatrix;

接下来是定义图了,上面定义的都是图的基本元件,现在要组合一个图的数据类型。

// 定义图
typedef struct
{
    int vexs[MAX_VERTEX_NUM];//顶点向量,存储顶点内容
    AdjMatrix arcs;//存储顶点之间的关系
    int vexnum, arcnum;//图的顶点数和边数
}MGraph;

这是一种自顶向下的思考方式,却是一种自底向上的实现策略。

给你一个图,你想知道图的哪些信息?
想不想知道图有几个顶点?
想不想知道顶点存了什么信息?
想不想知道图有多少边?是哪些顶点组成的边?

好吧,我想知道!
所以定义的图结构就是包括这些宏观数据的结构体。

以上,图的邻接矩阵存储定义完毕。

但是,只说这个定义,没什么作用,还需要真的能建起来才是靠谱的构建。

void CreateGraph(MGraph *G)//构造图
{
    printf("请输入顶点数,边数:\n");
    scanf("%d%d",&G->vexnum,&G->arcnum);//输入图的顶点数目和边数目

    // printf("请输入顶点内容,顶点数为%d\n",G->vexnum);

    for(int i = 0; i < G->vexnum; i++)//初始化图的顶点
    {
        // printf("输入结点%d: ",i);
        scanf("%d",&G->vexs[i]); //输入顶点信息:a,b,c,d...,假设顶点值各不相同
    }
    printf("结点内容输入完毕!---\n");

    //初始化邻接矩阵
    for(int i = 0; i < G->vexnum; i++)
    {
        for(int j = 0; j < G->vexnum; j++)
        {
            G->arcs[i][j].adj = INF;//设置顶点间的距离都是INF
        }
    }

    //构造邻接矩阵
    printf("请输入边:\n");
    for(int k = 0; k < G->arcnum; k++)
    {
        // printf("请输入边:\n");
        int u,v;//u,v分别为顶点存储内容
        int w; //w是边的权值
        scanf("%d %d %d",&u,&v,&w); //输入边
        // getchar();
        //根据顶点内容找到结点的下标--在一维数组中寻找
        int i = LocateElem(G,u);
        int j = LocateElem(G,v);
        G->arcs[i][j].adj = w;
        G->arcs[j][i].adj = w; //无向,所以对称边也赋值
    }
}

这个代码意义非常明确,告诉计算机多少个点,点的信息是什么(按照你输入的顺序存储到以为数组中),多少条边,请一个一个输入边的两边的点事谁,权值多大。就这样,一个真的活在计算机的存储系统的图就建造好了!

加入一点想象。

MGraph* Prim(MGraph *MG)

    MGraph *G = (MGraph*)malloc(sizeof(MGraph));
    if(G == NULL)
    {
        printf("ERROR\n");
    }
    G->vexnum = MG->vexnum;
    G->arcnum = 0;
    //清洗掉权值数据,赋值为INF
    for(int i = 0; i < G->vexnum; i++)
    {
        for(int j = 0; j < G->vexnum; j++)
        {
            G->arcs[i][j].adj = INF;//设置顶点间的距离都是INF
            G->arcs[j][i].adj = INF;//逆向清洗连接为INF
        }
    }
    //顶点的值赋予G
    for(int i = 0; i < MG->vexnum;i++)
    {
        G->vexs[i] = MG->vexs[i];
    }
    //设定一个标记数组,标记结点是否在树中
    bool NodeInTree[MAX_VERTEX_NUM];

    int count;//记录已经加入到树中的结点数目
    for(int i = 0; i < MAX_VERTEX_NUM; i++)
    {
        NodeInTree[i] = FALSE; //初始时没有一个结点在树中
    }

    //任意选择一个结点
    int u,v;
    u = G->vexnum / 2; //从中间开始进行构造树
    v = MAX_VERTEX_NUM;
    NodeInTree[u] = TRUE; //标记结点选中放到树中
    count = 1;
    while(count < G->vexnum) //直到加入树中的结点数与G.vexnum相等为止
    {

        //寻找U,V-U之间最小边,两层for循环
        int i = 0,j = 0;
        int MinEdge = INF;//初始化
        //while循环和for循环都是在满足的情况下遍历!!不然直接就跳过去了
        for(i = 0; i < G->vexnum; i++)
        {
            for(j = 0; j < G->vexnum; j++)
            {
                if((NodeInTree[i] == TRUE) && (NodeInTree[j] == FALSE))//i记作在树中的结点,j记作不在树中的结点
                {
                    if(MinEdge > MG->arcs[i][j].adj)
                    {
                        MinEdge = MG->arcs[i][j].adj;
                        u = i;
                        v = j;
                    }
                }
            }
        }
        G->arcs[u][v].adj = G->arcs[v][u].adj = MG->arcs[u][v].adj; //边加入树中
        G->arcnum++;//成功加入一条边
        NodeInTree[v] = TRUE;//结点加入树中
        count++;//又多了一个结点   
    }
    free(MG);
    return G;
}

这段便是我们要做的Prim算法的核心了。

算法步骤:
初始化:向空树T = (VT,ET)中添加图G = (V,E)的任何一个顶点u0,VT = {u0},ET ≠∅;
循环(重复直到VT = V):从图G中选择满足{(u,v)|u∈VT,v∈V-U}且具有最小权值的边(u,v),修改VT,ET集合,VT = VT∪{v},ET = ET ∪ {(u,v)};

//Prim算法
void Prim(G,T)
{
    T = NULL; //初始化空树
    U = {w}; //添加任一顶点w
    while((V-U) != 0)//树还没有拿到全部结点
    {
        MinEdge = (u,v);//u是U中结点,v是V-U中结点
        T = T + (u,v); //边加入树
        U = U + v; //点加入树
    }
}

算法的时间复杂度是O(V^2),不依赖边集。

适用场景:边稠密的图。

简单易懂。但是如何翻译成C语言,请看上面的代码,我不想说了,我只想说一下犯的一点很幼稚的错误:
首先,我得说,for和while循环都是在条件满足的时候才去执行。
若是条件不满足,才不会下去执行!

这个我测试的时候,看到循环总是直接跳过,很是纳闷。终于想通。

其次,讲一下在函数中传递结构体作为参数时的策略:

函数参数默认是通过四个寄存器r0,r1,r2,r3传递,多余的参数将压入栈中进行传递。
同理,对于一个结构体,如果其字节数少于32字节(4个寄存器的总容量),按照正常的方式通过r0到r3寄存器进行传递。多余32个字节就将多余的部分分配在栈中进行传递。
所以最好还是传递指针,高效(省去建立备份的时间),又节省栈空间。

想通一件事情的成就感是一种,做通一件事情的成就感又是另一种!

希望也对你有所启发,不仅仅是这个算法。

HOPE.

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Prim算法是一种基于贪心思想的最小生成树算法,其思路是从图中任意选一个顶点作为起点,然后逐步扩张生成树,直到生成包含所有顶点最小生成树为止。具体实现步骤如下: 1. 选取一个起始点,将其放入已访问集合中; 2. 从已访问集合中的所有点出发,找到与其相邻且不在已访问集合中的点中权值最小的边,将该点加入已访问集合中,并将该边加入生成树中; 3. 重复上述步骤,直到已访问集合中包含所有顶点。 下面是Matlab代码实现Prim算法: ```matlab function [MST, cost] = prim(adj_matrix) % adj_matrix: 图的邻接矩阵 % MST: 生成树的邻接矩阵 % cost: 生成树的总权值 n = length(adj_matrix); MST = zeros(n, n); % 初始化生成树的邻接矩阵 visited = zeros(1, n); % 标记点是否已访问 visited(1) = 1; % 选取第一个点作为起始点 % 循环n-1次,每次找到一条最小权值的边 for i = 1:n-1 min_cost = inf; from = 0; to = 0; % 遍历已访问集合中的所有点 for j = 1:n if visited(j) == 1 % 遍历未访问集合中的点,找到权值最小的边 for k = 1:n if visited(k) == 0 && adj_matrix(j, k) ~= 0 && adj_matrix(j, k) < min_cost min_cost = adj_matrix(j, k); from = j; to = k; end end end end % 将找到的边加入生成树中 MST(from, to) = min_cost; MST(to, from) = min_cost; visited(to) = 1; end cost = sum(sum(MST)); % 计算生成树的总权值 ``` 其中,邻接矩阵adj_matrix中,0表示两个顶点之间没有边,非0表示两个顶点之间有一条边,其权值为非0的数值。返回值MST为生成树的邻接矩阵,cost为生成树的总权值。 ### 回答2: Prim算法是一种用于解决最小生成树问题的算法Matlab也提供了相应的函数来实现Prim算法Prim算法的基本思想是从一个起始节点开始,逐步扩展最小生成树的边集合,直到包含所有节点为止。算法每次选择一个节点,并将该节点与当前最小生成树的集合中的节点进行连接,选择与最小生成树集合中节点连接的边中权值最小的边,并将该边加入最小生成树的边集合中。然后再选择另一个节点,重复上述步骤,直到所有节点都连接到最小生成树中为止。 在Matlab中,可以使用graph函数创建一个图,然后使用minspantree函数结合Prim算法来计算最小生成树。具体步骤如下: 1. 创建一个图对象,使用graph函数,传入节点和边的信息。 ``` G = graph(nodes, edges); ``` 其中,`nodes`表示节点的信息,`edges`表示边的信息。 2. 调用minspantree函数,传入图对象G,以及'Method'参数指定Prim算法。 ``` [T, pred] = minspantree(G, 'Method', 'prim'); ``` 返回的结果包括最小生成树的边集合T和前驱节点pred的矩阵。 3. 可以使用plot函数将最小生成树可视化。 ``` plot(G, 'EdgeLabel', G.Edges.Weight); highlight(G, T); ``` 这样可以将图G以及最小生成树T的边标注权值,并将最小生成树的边高亮显示。 以上就是使用Matlab实现Prim算法求解最小生成树的基本步骤。在具体应用中,根据不同的需求,还可以对函数的参数进行相应的调整,以满足具体问题的要求。 ### 回答3: Prim算法是一种解决最小生成树问题的经典算法。其思想是从图中选择一个顶点作为起始点,并将该点加入到生成树中。然后,在与生成树相连的边中选择一个权值最小的边,并将其顶点加入到生成树中。不断重复此步骤,直到所有顶点都加入到生成树中。 具体实现Prim算法需要用到一个数组来记录每个顶点的权值,初始值都设置为整型的最大值,表示当前不可达。然后,选择一个顶点作为起始点,设置其权值为0,表示已经加入生成树。 接下来,对于与起始点相连的顶点,更新其权值。如果更新后的权值小于原有权值,说明找到了更短的路径,则更新其路径和对应的边。重复上述步骤,直到所有顶点都加入到生成树中。 在Prim算法中,需要使用辅助数组来记录已经加入生成树的顶点,以及记录每个顶点对应的最短路径和对应的边。通过这些辅助数组,可以方便地实现Prim算法。 总之,Prim算法是一种高效的最小生成树算法,通过不断选择权值最小的边,将所有顶点加入生成树中。它的时间复杂度为O(V^2),其中V表示顶点数。在实际应用中,Prim算法可以用来构建网络拓扑结构、解决通信网络问题等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值