求图的最小生成树

上一小节讨论了一些图的基本概念。其中提到,如果有两幅图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算法跟胜一筹。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值