求图的最小生成树

来源:互联网 发布:java struct 结构体 编辑:程序博客网 时间:2024/05/21 05:39

上一小节讨论了一些图的基本概念。其中提到,如果有两幅图G=(V,E),G'=(V',E'),如果V'∈V,E'∈E,则称G'是G的子图。如果V'=V,且E'∈E(即顶点不少,边少了几条),那么G'是G的生成子图。特别的,如果生成子图是一棵树,即满足定点数 = 边树+1,那么这棵树称为生成树。
最小生成树的概念是基于“带权图”的。即图中每条边上都有特定的权值,这样的图又称为网。最小生成树指的是所有生成树中,权值之和最小的树。
当然,这个概念绝非数学家们自己胡思乱想出来的,它能对很多实际的问题建模:比如有A,B,C,D,E,F,6个地方。我们需要修路让他们之间可以相互连通。但是各个节点之间修路的花费可能不一样,n个地方如果两两相通,一共有n(n-1)/2条路,我们想在其中找出n-1条,让它们的花费最小。那么如何选择出这n-1条路径呢?有两种经典的算法Kruskal算法和Prim算法。

先看定义的一些基本的数据结构(总体上跟图的差不多)


 

#include <stdio.h>#include <limits.h>#include <malloc.h>#define MAX 10//带权图的基本数据结构typedef int** adjacentMatrix;typedef struct WeightedGraph{adjacentMatrix matrix;char* vertex;int vertexNumber;int arcNumber;}wGraph;//基本操作声明//初始化图wGraph initWgraph(int );//销毁图void destroyWgraph(wGraph*);//增加一条边bool addArc(wGraph* ,char ,char ,int );//删除一条边bool deleteArc(wGraph* ,char ,char );//显示邻接矩阵void printfWgraph(wGraph* );//Kruskal算法相关函数int findMiniWeightArc(wGraph* ,char*,char*);void do_dfs(wGraph* ,int );bool is_loop(wGraph* );void Kruskal(wGraph* ,wGraph* );

 

 

Kruskal算法的基本思路如下:
1.将图的边集合按照权值大小排序(可选)。
2.从E中挑出一个权值最小的边e,权值为we
3.将e加入到生成树的边的集合T中。
4.加入e之后判断T是否形成回路
5.若T的边数<n-1,则转2,否则退出

程序如下:


 

wGraph initWgraph(int n){wGraph g;g.vertexNumber = n;g.arcNumber = 0;g.vertex = (char*)malloc(sizeof(char) * n);char start = 'A';for(int i = 0; i < n;++i)g.vertex[i] = start + i;g.matrix = (int**)malloc(sizeof(int*) * n);for(int i = 0; i < n;++i)g.matrix[i] = (int*)malloc(sizeof(int) * n);//邻接矩阵的初始为为最大值for(int i = 0; i < n;++i)for(int j = 0; j < n;++j)g.matrix[i][j] = INT_MAX;return g;}void destroyWgraph(wGraph* g){free(g->vertex);g->vertex = NULL;for(int i = 0; i < g->vertexNumber;++i)free(g->matrix[i]);free(g->matrix);g->matrix = NULL;g->arcNumber = -1;g->vertexNumber = -1;}bool addArc(wGraph* g,char vertex1,char vertex2,int weight){int i = vertex1 - 'A';int j = vertex2 - 'A';if(i < 0 || i > g->vertexNumber || j < 0 || j > g->vertexNumber){printf("vertexes does not exsist!\n");return false;}else{g->matrix[i][j] = g->matrix[j][i] = weight;g->arcNumber++;return true;}}bool deleteArc(wGraph* g,char vertex1,char vertex2){int i = vertex1 - 'A';int j = vertex2 - 'A';if(i < 0 || i > g->vertexNumber || j < 0 || j > g->vertexNumber){printf("vertexes does not exsist!\n");return false;}if(INT_MAX == g->matrix[i][j] ){printf("there is no arc between vertexes!\n");return false;}else{g->matrix[i][j] = INT_MAX;g->matrix[j][i] = INT_MAX;g->arcNumber--;return true;}}void printfWgraph(wGraph* g){printf("     ");for(int i = 0 ; i < g->vertexNumber;++i)printf("%-4c ",g->vertex[i]);for(int i = 0; i < g->vertexNumber;++i){printf("\n");printf("%-4c ",g->vertex[i]);for(int j = 0; j < g->vertexNumber;++j){if(INT_MAX == g->matrix[i][j]  )printf("NULL ");elseprintf("%-4d ",g->matrix[i][j]);}}printf("\n");}//找完以后需要打上标记,否则下一次还会找他int findMiniWeightArc(wGraph* g,char *vertex1,char *vertex2){int miniWeight = INT_MAX;int vex1;int vex2;for(int i = 0; i < g->vertexNumber;++i){for(int j = 0; j< g->vertexNumber;++j){//printf("测试边%c%c,它的权值为%d\n",g->vertex[i],g->vertex[j],g->matrix[i][j]);if(g->matrix[i][j] < miniWeight && g->matrix[j][i] < miniWeight && g->matrix[i][j] != -1 && g->matrix[j][i] != -1){miniWeight = g->matrix[i][j];vex1 = i;vex2 = j;}}}//打上标记g->matrix[vex1][vex2] = -1;g->matrix[vex2][vex1] = -1;*vertex1 = 'A' + vex1;*vertex2 = 'A' + vex2;return miniWeight;}//判断加入以后是否形成回路//visit1表明该节点是否被访问过bool visit1[MAX];//visit2表明该节点在这次搜索中是否被访问过bool visit2[MAX][MAX];bool LOOP = false;void do_dfs(wGraph* g,int i){visit1[i] = true;for(int j = 0; j < g->vertexNumber;++j){//对当前节点,挑选可以访问的节点if(INT_MAX != g->matrix[i][j]){//如果这个次访问为回溯,则跳过这个节点if(visit2[i][j] == true)continue;else{if(visit1[j] == false){visit2[i][j] = true;visit2[j][i] = true;do_dfs(g,j);}elseLOOP = true;}}}}bool is_loop(wGraph* g){//重置LOOPLOOP = false;//for(int i = 0; i < g->vertexNumber;++i){visit1[i] = false;}for(int i = 0; i < g->vertexNumber;++i){for(int j = 0; j < g->vertexNumber;++j){visit2[i][j] = false;}}for(int i = 0; i < g->vertexNumber;++i){if(false == visit1[i])do_dfs(g,i);}return LOOP;}void Kruskal(wGraph* g,wGraph* tree){//先初始化树*tree = initWgraph(g->vertexNumber);while(tree->arcNumber < g->vertexNumber-1){char vertex1;char vertex2;int weight = findMiniWeightArc(g,&vertex1,&vertex2);addArc(tree,vertex1,vertex2,weight);if(is_loop(tree) == true){printf("被删除的边为:%c,%c\n",vertex1,vertex2);deleteArc(tree,vertex1,vertex2);}elseprintf("挑出来的边为:%c,%c\n",vertex1,vertex2);//printfWgraph(tree);}}


再看Prim算法:
1.设顶点集合为V,从V中任取一个顶点,加入U中,并将这个节点从V中删去。
2.在U与V构成的所有边中,选择一个权值最小的,加入树的边集合E
3.将边E的另一个节点vx也加入集合U中。
4.若V不为空,则转2

 

//Prim算法相关的函数void selectMiniWeightArc(wGraph* g,bool* u,bool* v,int* indexV,int* indexU);void Prim(wGraph* g,wGraph* t,char Start);


 

void Prim(wGraph* g,wGraph* t,char Start){//将t初始化*t = initWgraph(g->vertexNumber);bool *V  = (bool*)malloc(sizeof(bool) * g->vertexNumber);//V集合初始值为true表示这个集合里的元素都可用,false表示集合中元素被删掉了for(int i = 0; i < g->vertexNumber;++i)V[i] = true;int cnt = g->vertexNumber; bool *U = (bool*)malloc(sizeof(bool) * g->vertexNumber);//U集合初始值为false,表明这个集合中还没有任何元素,true表示这个元素可用for(int i = 0; i < g->vertexNumber;++i)U[i] = false;//每次从V中选中一个元素,则把它的V[i]设为false,把U[i]设为trueint start = Start-'A';V[start] = false;--cnt;U[start] = true;while(cnt >0){int indexV;int indexU;//在U与V-U中寻找权值最小的边selectMiniWeightArc(g,U,V,&indexV,&indexU);char vertex1 = indexV+'A';char vertex2 = indexU+'A';//printf("挑出V中的节点%c,U中的节点%c\n",vertex1,vertex2);addArc(t,vertex1,vertex2,g->matrix[indexV][indexU]);V[indexU] = false;U[indexU] = true;--cnt;}free(U);free(V);}void selectMiniWeightArc(wGraph* g,bool* u,bool* v,int* indexV,int* indexU){int min = INT_MAX;//打印一下两个数组的现状printf("V:");for(int i = 0; i < g->vertexNumber;++i)printf("V[%d] = %d  ",i,v[i]);printf("\n");printf("U:");for(int i = 0; i < g->vertexNumber;++i)printf("U[%d] = %d  ",i,u[i]);printf("\n");//遍历整个图for(int i = 0; i < g->vertexNumber;++i){for(int j = 0; j < g->vertexNumber;++j){//i是V中的,j是U中的if(false == v[i] && true == u[i] && true == v[j] && false == u[j]){if(g->matrix[i][j] < min){min = g->matrix[i][j];*indexV = i;*indexU = j;}}}}}


程序中需要注意,当两个顶点不连通是,我对他们之间的权值设置为INT_MAX,但是在打印时,为了好看一点,显示NULL。具体的算法程序是严格按照算法的步骤写的,基本没有什么花架子。唯一需要说明的是,在Kruskal算法中,需要检测是否形成回路回路,这个问题可让我费了一番周折。因为没有学过离散数学,所以也没有用什么好的办法,而是用了一个比较朴素的办法:深度优先遍历整个图:从A找到C以后,把A->C与C->A的两条路都标记了起来。然后继续向下查找,如果最后能找回来,则说明有回路。

具体的查找过程可以通过注释起来的打印信息看出来,这里就不罗嗦了。

最后简单的比较这两种算法:Prim算法使用的数据结构稍微有点复杂,用两个数组记录了顶点是否被使用过,但是它很好地避免了回路检测这个问题;而Kruskal算法则上手比较容易,更符合我们的基本思维,但是回路检测过程却效率低下(这也与我的回路检测算法有关)。总体上来说,Prim算法跟胜一筹。

原创粉丝点击