最小生成树及其构造方法

时间:2021-02-11 11:39:51

ps:好久没写新文章了,来到了广州见习,没有网络,也不知道这学校怎么想的!!接下来进入正题

何谓最小生成树

对于一个带权连通无向图G中的不同生成树,各棵树的边上的权值之和可能不同,边上的权值之和最小的树称之为该图得最小生成树

构造方法

求图得最小生成树的俩个算法,即普利姆算法和克鲁斯卡尔算法

普利姆算法(Prim)

在了解普利姆算法之前,先想想我们最开始的目的是什么?俗气一点说就是画路线图,而且这个路线必须是最短的。

思路

先用抽象一点的语言描述,就是现在我们先拿出图中的第一个顶点(其实可以是任意顶点),然后把这个顶点放进集合A中,然后将这个顶点与集合外的顶点连成的线的权值做比较,把那个能连权值最小的线的顶点也放进集合里面。然后再把集合里面的俩个顶点与集合外的顶点继续比较,重复此步骤。可以参考:
最小生成树及其构造方法

第一步:从①开始,①进集合,用与集合外所有顶点能构成的边中找最小权值的一条边
①——②权6
①——③权1 -> 取①——③边
①——④权5



第二步:③进集合,①,③与②,④,⑤,⑥构成的最小边为
①——④权5
③——⑥权4 -> 取③——⑥边

第三步:⑥进集合,①,③,⑥与②,④,⑤构成的各最小边
①——②权6
③——②权5
⑥——④权2 -> 取⑥——④边

第四步:④进集合,①,③,⑥,④与②,⑤构成的各最小边
①——②权6
③——②权5 -> 取③——②边
⑥——⑤权6

第四步:②进集合,①,③,⑥,②,④与⑤构成的各最小边
②——⑤权3 -> 取②——⑤边

没错上面的说法很好,但实际中的代码跟上面的思想是一样,但就有点难理解,我们换一种说法,从代码的角度来分析。
假如我们现在拿到手的是一个邻接矩阵存储方法的图(假设这个图有n个顶点)
1.那么首先我们先实例化一个数组A,一个数组B。先这么规定,数组A存储的是n-1条边的权值。那么另外一个数组就是存储边的起始顶点。举个例子A[1]=3,B[1]=4,代表的就是第一个顶点所连接的最短的边的权值是3,然后这条最短边是和第4个顶点连接而成的。
2.那么有了这俩个数组,事情就变得好办很多了。B[i]=k代表的就是i与k连接可以形成最小权值的边,而A[i]数组就代表i顶点与这个k(k=B[i])连接的边的那个最小权值。这里有点绕口。
从实际例子来讲思路比较好,先看看下图:
3.先进行初始化

for(i=0;i<g.n;i++)
{
lowcost[i] = g.edges[0][i];
closest[i] = 0;
}

此时lowcost数组的意义就是各个顶点到0顶点的距离,closest数组的原理就是当前lowcost中的各个数值是由哪俩个顶点连起来的,可见初始化过程使得closest的每个值都为0,也就是lowcost中的每条边都是和0顶点连起来的.
4.接下来,取出lowcost中最小的边,如下:

min = INF;
//先遍历出lowcost里面最小的拿出来
for(j=0;j<g.n;j++)
{
if(lowcost[j]!=0 && lowcost[j]<min)
{
min = lowcost[j];
k = j;
}
}
printf("边(%d,%d)权为:%d\n",closest[k],k,min);
lowcost[k] = 0;

取出的这条边就是顶点0与顶点k连接而成的。先用我们的脑子想一下,取出这条最短边之后我们应该做些什么事?
5.没错,取出第二条最短的边,我们需要把能和顶点0和顶点k连线的那些边取出来然后进行比较存储在lowcost数组中。
这也就跟我们第一个抽象化的思路一样,顶点0和顶点k在集合里面,与集合外面的顶点进行比较。
那么我们该怎么做这个比较的操作呢?

for(j=0;j<g.n;j++)
{
if(g.edges[k][j]!=0 && lowcost[j]>
g.edges[k][j])
{5
lowcost[j] = g.edges[k][j];
closest[j] = k;
}
}

g.edges[k][j]就是顶点j到顶点k的距离,lowcost[j]就是顶点j到顶点0的目前最短距离(在第一次大循环中,由于closest[j]都等于0,所以都是与顶点0的距离),拿这俩个数值做比较,得出一个更小的距离存储到lowcost中。
那么整个算法就是:

代码

#define INF 32767;
void Prim(MGraph g,int v)
{
int lowcost[MAXV];
int min;
//就是对应的元素下标的顶点的连接的上一个顶点
int closest[MAXV];
int i,k;
for(i=0;i<g.n;i++)
{
lowcost[i] = g.edges[v][i];
closest[i] = v;
}
for(i=1;i<g.n;i++)
{
min = INF;
//先遍历出lowcost里面最小的拿出来
for(j=0;j<g.n;j++)
{
if(lowcost[j]!=0 && lowcost[j]<min)
{
min = lowcost[j];
k = j;
}
}
printf("边(%d,%d)权为:%d\n",closest[k],k,min);
lowcost[k] = 0;
//接下来做的就是把集合里的俩个顶点跟集合外的顶点组成的边的权值做比较
for(j=0;j<g.n;j++)
{
if(g.edges[k][j]!=0 && lowcost[j]>g.edges[k][j])
{
lowcost[j] = g.edges[k][j];
closest[j] = k;
}
}
}
}

总结

对于普利姆算法来说,只要理解lowcost数组和closest数组,基本大概思路就已经清晰了

克鲁斯卡尔算法(Kruskal)

克鲁斯卡尔算法的思想就是先把所有边取出来,然后按从小到大排序,每次取出一条边,判断是否形成回路,如果是就丢弃,否则这条边就是我们想要的权值最小的边。

思路

核心-怎么样去判断回路
在这里需要简单地说一下思路。上面已经说了,我们有一个数组是按权值从小到大存储边的(这里先不要去考虑怎么存储边),然后我们每次拿出来一条边,这时候就需要判断这条边会不会跟我们前面已选择的边形成回路,因此,每次取出边的时候,我们需要把它的“原始顶点”存放到vset数组中,那么怎么存放呢?
假设我们现在有边ab,bc,cd都被取出来。那么这三条边就属于一个连通分量,也就是到时是连在一起的。那么ad这俩个顶点就不能连接起来了,很明显会构成一个回路,计算机应该怎么判断呢。可以这样规定,ab连在一起了,所以b的parent是a,bc连在一起了,所以c的parent是b,cd连在一起了所以d的parent是c。那么我们可以通过下面的代码来判断是否构成回路:

int Find(int *parent,int f)
{
while(parnet[f]>0)
f = parent[f]
return f;
}

上面的代码实现了查找一个连通分量的其实顶点,通过上面的代码,我们可以知道d
的“原始顶点”是a,a的原始顶点也是a,所以相等,并不能连线。

代码

所以整个算法就是:

int Find(int *parent,int f)
{
while(parnet[f]>0)
f = parent[f]
return f;
}

Typedef struct
{
int begin;
int end;
int weight;
}Edge;

void Kruskal(MGraph g)
{
Edge E[MaxSize];
int vset[MaxSize];
int i,k,j;
for(i=0;i<g.n;i++)
for(j=0;j<g.n;j++)
if(g.edges[i][j]!=0 && g.edges[i][j]!=INF)
{
E[k].begin = i;
E[k].end = j;
E[k].weight = g.edges[i][j];
k++;
}
InsertSort(E,g.e);//排序
for(i=0;i<g.n;i++) vset[i]=0;
//要区n-1条边出来
k = 1;
//每次取出一条边,所以要有一个元素记住现在取到第几条边了
j = 0;
while(k<g.n)
{
u1 = E[j].begin;
u2 = E[j].end;
sn1 = Find(vset,u1);
sn2 = Find(vset,u2);
if(sn1!=sn2)
{
vset[sn2] = sn1;
printf();
k++;
}
j++;
}
}