本文主要是介绍最小生成树算法之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;//清洗掉权值数据,赋值为INFfor(int i = 0; i < G->vexnum; i++){for(int j = 0; j < G->vexnum; j++){G->arcs[i][j].adj = INF;//设置顶点间的距离都是INFG->arcs[j][i].adj = INF;//逆向清洗连接为INF}}//顶点的值赋予Gfor(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;//清洗掉权值数据,赋值为INFfor(int i = 0; i < G->vexnum; i++){for(int j = 0; j < G->vexnum; j++){G->arcs[i][j].adj = INF;//设置顶点间的距离都是INFG->arcs[j][i].adj = INF;//逆向清洗连接为INF}}//顶点的值赋予Gfor(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}; //添加任一顶点wwhile((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.
这篇关于最小生成树算法之Prim算法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!