图——最小生成树的两种算法

普里姆算法

普里姆算法是典型的构造最小生成树的算法

下面说到的V和V-U都是MST性质里来的,如果不知道MST,点击 → \rightarrow MST个人理解心得

1.构造过程

  1. 从所有顶点中任选一个顶点出发,将该顶点划分到U中,其余顶点就属于V-U了。
  2. 将该顶点试着与其余顶点连接,找出权值最小的边并连接上。并将该边的另一个顶点并入U中。
  3. 从U中所有顶点出发,都试着与其余顶点连接,找出最小权值的边,连接并并入该边的另一个顶点到U中。(如果U中有两个顶点有公共的顶点,那么也取权值最小的边;如果权值一样,则任取一条)。
  4. 直到所有顶点形成一个连通图(生成树),此时也必定是一棵最小生成树

例如:在这里插入图片描述
就上图而言,也可以自己试着从其他的顶点出发构造一下

2.算法实现

假设一个无向网G以邻接矩阵形式存储,从顶点u出发构造G的最小生成树T,要求输出T的各条边。

为实现这个算法需要附设一个辅助数组 closeedge,该数组中每个分量含有两个域:lowcost和adhvex。其中lowcost存储最小边上的权值,adjvex存储最小边在U中的那个顶点。注意:closedge[i]中的i指的是V-U中的顶点,adjvex指的是U中的顶点。该辅助数组的定义如下:

struct 
{
	VexTexType adjvex;			//最小边在U中的那个顶点
	ArcType lowcost;			//最小边上的权值
}closedge[MVNum];

这个辅助数组是十分重要的,整个算法的关键之处就在于对这个结构体数组的理解。简单来说,这个数组是存放V-U中顶点到U中顶点的边的信息。下面我们根据算法来进一步理解。

算法分析:

  1. 首先,closedge数组的下标从0~i相当于V-U中顶点的下标。从任一顶点u出发,由于我们知道的是顶点的信息(u),而不知道该顶点在图中的位置(也就是下标),需要LocateVex函数来获取该顶点的下标,并将其赋值给k,k就是顶点u的下标。
  2. 然后我们需要对closeedge数组进行初始化,也就是将closedge数组每各分量都表示为其余顶点(V-U中的所有顶点)到顶点u的信息。这句话的意思就是将closedge[i].adjvex=u,且closedge[i].lowcost为其余顶点到顶点u的边的权值。
  3. 由于前面初始化不包含对closedge[k]的初始化,又因为closeege[k]相当于自己到自己的边的信息,因此closedge[k].lowcost=0。
  4. 然后就是进行一个n-1次的循环:
  • 首先我们需要从closedge数组中找出权值最小的边(closedge数组中都是顶点u到其余顶点的边的权值),找到之后我们需要得到closedge[k]中的k,这个k相当于是V-U中的顶点的下标。这里可以设计一个Min函数来实现
  • 找到之后就输出两个顶点,并将新顶点并入U中,也就是close[k].lowcost。
  • 接着就是最关键的一步。首先,closedge中仍存储的是各个顶点到顶点u的边的信息(除了对closedge[k]的改动,因为下标为k的顶点相较于V-U中其他顶点到顶点u的权值最小),然后对于新并入的顶点,又将它到其余顶点的边的权值与closedge数组中的权值比较,若比数组中的权值要小,则将新并入顶点到其余顶点的边的权值代替数组中的权值,然后需要将closedge数组中对应下标改为k。然后又开始下一个循环。

代码实现(总代码在最后):

void MiniSpanTree_Prim(AMGraph G,VerTexType u)
{
	int k,i,j;
	k=Locate(G,u);							//k为顶点u的下标
	for(j=0;j<G.vexnum;j++)					//对V-U中每个顶点的初始化,j为V-U中顶点的下标
	{
		if(j!=k)
		{
			closedge[j].adjvex=u;
			closedge[j].lowcost=G.arcs[k][j];
		}
	}
	
	closedge[k].lowcost=0;					//将其置为0就代表将这个顶点并入U中
	
	//对其余n-1个顶点的操作
	for(i=1;i<G.vexnum;i++)
	{
		k=Min(closedge);					//表示顶点u到下标为k的顶点相较于其余顶点的权值最小
		u0=closedge[k].adjvex				//表示顶点u的下标,因为前面初始化adjvex=u
		v0=G.vexs[k];						//相当于另一个顶点的下标就是k
		printf("(%d,%d)\n",u0,v0);
		closedge[k].lowcost=0;				//将该顶点并入U中
		for(j=0;j<G.vexnum;j++)
		{
			if(G.arcs[k][j]<closedge[j].lowcost)
			{
				closedge[j].adjvex=G.vexs[k];
				closedge[j].adjvex=G.arcs[k][j];
			}
		}
	}
}

如果还看不懂,可以试着自己动手找个具体的例子画一下每次循环后closedge数组中的各域的值。那么再强调一下:假如从v1出发(其余顶点为v2、v3、v4…),那么closedge数组的初始化,也就是该数组中存储的是其余各个顶点到v1的边的信息(adjvex=v1,lowcost=权值),并且这里是默认各个权值都是最小的。然后找到权值最小的,并记住在closedge数组中的下标,在这里假如是v4。然后打印v1,v4。最后就是再将v4到其余顶点(除自身和v1,因为它们都在U中)的权值与数组中的权值比较,若比数组中某个分量更小,就替换。

3. 总代码

#include <stdio.h>
#define MVNum 20
#define MaxInt 32767
typedef char VerTexType;
typedef int ArcType;

//辅助数组的定义
struct CE
{
	VerTexType adjvex;
	ArcType lowcost;
}closedge[MVNum];


//邻接矩阵的存储表示
typedef struct
{
	VerTexType vexs[MVNum];				//存储顶点
	ArcType arcs[MVNum][MVNum];			//邻接矩阵
	int vexnum,arcnum;
}AMGraph;


//获取顶点的下标
int  LocateVex(AMGraph G,VerTexType v)
{
	int i=0;
	while(G.vexs[i]!=v)
	{
		i++;
	}
	return i;
}

//Min函数
int Min(struct CE *p)
{
	int j=1,k=0,min=MaxInt;
	while(j<MVNum)
	{
		if((p+j)->lowcost!=0&&(p+j)->lowcost<min)
		{
			min=(p+j)->lowcost;
			k=j;				
		}
		j++;	
	}
	return k;
}


//邻接矩阵构建无向网
int  CreateUDN(AMGraph G)
{
	ArcType i, j, k,w;
	VerTexType v1, v2;
	printf("请输入总顶点和总边数:");
	scanf(("%d,%d"), &G.vexnum, &G.arcnum);
	printf("请输入顶点的信息:");
	for (i = 0; i < G.vexnum; i++)
		scanf("%c", &G.vexs[i]);
	for (i = 0; i < G.vexnum; i++)
		for (j = 0; j < G.vexnum; j++)
			G.arcs[i][j] = MaxInt;
	for (k = 0; k < G.arcnum; k++)
	{
		printf("请输入一条边依附的顶点及权值:");
		scanf("%c,%c,%d", &v1, &v2, &w);
		i = LocateVex(G, v1);
		j = LocateVex(G, v2);
		G.arcs[i][j] = w;
		G.arcs[j][i] = G.arcs[i][j];
	}
	return 0;
}


//普里姆算法构造最小生成树
void MiniSpanTree_Prim(AMGraph G,VerTexType u)
{
	int k,i,j,u0,v0;
	k=LocateVex(G,u);							//k为顶点u的下标
	for(j=0;j<G.vexnum;j++)					//对V-U中每个顶点的初始化,j为V-U中顶点的下标
	{
		if(j!=k)
		{
			closedge[j].adjvex=u;
			closedge[j].lowcost=G.arcs[k][j];
		}
	}
	
	closedge[k].lowcost=0;					//将其置为0就代表将这个顶点并入U中
	
	//对其余n-1个顶点的操作
	for(i=1;i<G.vexnum;i++)
	{
		k=Min(closedge);					//表示顶点u到下标为k的顶点相较于其余顶点的权值最小
		u0=closedge[k].adjvex;		//表示顶点u的下标,因为前面初始化adjvex=u
		v0=G.vexs[k];						//相当于另一个顶点的下标就是k
		printf("(%d,%d)\n",u0,v0);
		closedge[k].lowcost=0;				//将该顶点并入U中
		for(j=0;j<G.vexnum;j++)
		{
			if(G.arcs[k][j]<closedge[j].lowcost)
			{
				closedge[j].adjvex=G.vexs[k];
				closedge[j].adjvex=G.arcs[k][j];
			}
		}
	}
}

这里没有写main函数,main函数的实现也很简单,只需调用函数输入顶点和边的信息,再调用其它函数。

如果上述代码有错,欢迎指正。

克鲁斯卡尔算法

如果普里姆算法中,构造最小生成树是将顶点一个一个地连起来,称为加点法,那么克鲁斯卡尔算法则是先列出全部顶点,然后将边一条一条地加上去,称为加边法

1. 构造过程

  1. 将一张连通网中的边按权值从小到大的顺序排序。
  2. 假想这张网,现在只有全部顶点没有一条边,每个顶点都是一个连通分量。
  3. 选择权值最小的一条边,若该边依附的两个顶点分别属于不同的连通分量(例如把一条边加到两个顶点上,这两个顶点原本都是两个单独的连通分量,连在一起后就是一个连通分量了,而 “不同” 相当于就是这边的两个顶点分别属于两个连通分量),则将这条边加入到图中;否则,舍弃这条边而去选择下一条权值最小的边。
  4. 重复步骤3,直到所有顶点组成一个连通图为止。

2. 算法实现

首先,我们仍然需要一个结构体数组来存储边的信息,其中应该包括该边依附的两个顶点和它的权值。然后,还有一个问题就是如何确定顶点间是否已“连通”?因此,我们应该考虑到来存储顶点的连通信息。如果两个及以上的顶点连在了一起,则它们的连通信息应该是相同的。那么为此,我们可以设置一个一维数组来存储它们的连通信息,即该值的初始化应是(1,2,3,…)这样不同的序号来代表它们最开始都是单独的一个连通分量。若相同,则改变这个值就行了。

下面为两个数组的存储表示:

struct ED
{
	VerTexType Head;			//边的起点
	VerTexType Tail;			//边的终点
	ArcType lowcost;			//边的权值
}Edge[arcnum];

int Vexset[MVNum];				//顶点的连通信息

算法分析:

  1. 将权值按从小到大的顺序排列,可以用一个循环嵌套一个循环的方式来实现
  2. 接下来就是加边,这里用一个循环来完成(注意,边的数量应等于无向网的中边的数量,这个算法是在不断地筛选出适合构造最小生成树的n-1条边)
  • 选出具有最小权值的一条边,并在Vexset数组中查找这条边依附的两个顶点v1,v2的连通信息vs1,vs2
  • 判断vs1和vs2是否相等,若相等则舍去这条边,选择下一条权值最小的边
  • 若不相等,则输出这条边,并将它们的连通信息改为相同的(将vs1改为vs2或将vs2改为vs1都行)

从算法分析来看,克鲁斯卡尔算法比普里姆算法更容易理解一些,下面直接给出具体算法

代码实现:

void MiniSpanTree_Kruskal(AMGraph G)			//该无向网也用邻接矩阵的存储结构
{
	int i,j,v1,v2,vs1,vs2;
	ArcType Mid;
	
	//权值的排序
	for(i=0;i<arcnum;i++)
		for(j=1;j<arcnum;j++)
			if(Edge[j].lowcost<Edge[i].lowcost)
			{
				Mid=Edge[j].lowcost;
				Edge[i].lowcost=Edge[j].lowcost;
				Edge[j].lowcost=Mid;
			}
	
	//Vexset数组的初始化
	for(i=0;i<G.vexnum;i++)
		Vexset[i]=i;							//各顶点的连通信息都是单独的一个值
		
		
	//Kruskal算法构造最小生成树	
	for(i=0;i<G.arcnum;i++)
	{
		v1=LocateVex(G,Edge[i].Head);
		v2=LocateVex(G.Edge[i].Tail);
		vs1=Vexset[v1];
		vs2=Vexset[v2];
		if(vs1!=vs2)
		{
			printf("(%d,%d)\n",v1,v2);
			for(j=0;j<G.vexnum;j++)
				if(Vexset[j]==vs2)				//这里的循环是为了方便一个连通分量中有多个顶点的情况
					Vexset[j]=vs1;				
		}
	}
}

总结

何为最小生成树?在回答这个问题之前先搞明白我们首先需要直到什么是生成树?生成树的本质上是一个连通子图,该图含有图中全部n个顶点,但只有n-1条边。而最小则是定义在n-1条边的权值加起来最小。

那么最小生成树有什么用呢?在生活中,如要在多个城市之间建立通信联络网,我们要考虑的问题就是如何在最节省经费的前提下建立这个通信网。也就是说,最小生成树的目的是使代价最小。

对于简单的图来说,自己算一下还是没啥问题的,但是如果是复杂的图,那么就比较难搞了。于是,这就需要来交给计算机让它以一定的算法去计算。而普里姆和克鲁斯卡尔算法正是两个构造生成树的算法,在上面已经介绍过了。

那么,这两种算法有啥区别?

普里姆算法实质上是加点法,而克鲁斯卡尔实际上是加边法。通过前面的代码来看(以及介绍的字数来看-_-),普里姆算法比克鲁斯卡尔算法要复杂得多。但话是这样说,这两个算法又各有优点。首先对于普里姆算法,其中有两个内循环,所以它的时间复杂度为 O ( n 2 ) O(n^2) O(n2),且与网中的边的数目无关,因此该算法适用于稠密网。对于克鲁斯卡尔算法,它的时间复杂度与边的数目有关,所以克鲁斯卡尔算法更适用于稀疏网

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值