求图的最小生成树

时间:2021-06-03 12:33:53

上一小节讨论了一些图的基本概念。其中提到,如果有两幅图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 ");
else
printf("%-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);
}
else
LOOP = true;

}
}

}
}

bool is_loop(wGraph* g)
{
//重置LOOP
LOOP = 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);

}
else
printf("挑出来的边为:%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]设为true
int 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算法跟胜一筹。