普里姆算法
普里姆算法是典型的构造最小生成树的算法
下面说到的V和V-U都是MST性质里来的,如果不知道MST,点击 → \rightarrow → MST个人理解心得
1.构造过程
- 从所有顶点中任选一个顶点出发,将该顶点划分到U中,其余顶点就属于V-U了。
- 将该顶点试着与其余顶点连接,找出权值最小的边并连接上。并将该边的另一个顶点并入U中。
- 从U中所有顶点出发,都试着与其余顶点连接,找出最小权值的边,连接并并入该边的另一个顶点到U中。(如果U中有两个顶点有公共的顶点,那么也取权值最小的边;如果权值一样,则任取一条)。
- 直到所有顶点形成一个连通图(生成树),此时也必定是一棵最小生成树
例如:
就上图而言,也可以自己试着从其他的顶点出发构造一下
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中顶点的边的信息。下面我们根据算法来进一步理解。
算法分析:
- 首先,closedge数组的下标从0~i相当于V-U中顶点的下标。从任一顶点u出发,由于我们知道的是顶点的信息(u),而不知道该顶点在图中的位置(也就是下标),需要LocateVex函数来获取该顶点的下标,并将其赋值给k,k就是顶点u的下标。
- 然后我们需要对closeedge数组进行初始化,也就是将closedge数组每各分量都表示为其余顶点(V-U中的所有顶点)到顶点u的信息。这句话的意思就是将closedge[i].adjvex=u,且closedge[i].lowcost为其余顶点到顶点u的边的权值。
- 由于前面初始化不包含对closedge[k]的初始化,又因为closeege[k]相当于自己到自己的边的信息,因此closedge[k].lowcost=0。
- 然后就是进行一个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. 构造过程
- 将一张连通网中的边按权值从小到大的顺序排序。
- 假想这张网,现在只有全部顶点没有一条边,每个顶点都是一个连通分量。
- 选择权值最小的一条边,若该边依附的两个顶点分别属于不同的连通分量(例如把一条边加到两个顶点上,这两个顶点原本都是两个单独的连通分量,连在一起后就是一个连通分量了,而 “不同” 相当于就是这边的两个顶点分别属于两个连通分量),则将这条边加入到图中;否则,舍弃这条边而去选择下一条权值最小的边。
- 重复步骤3,直到所有顶点组成一个连通图为止。
2. 算法实现
首先,我们仍然需要一个结构体数组来存储边的信息,其中应该包括该边依附的两个顶点和它的权值。然后,还有一个问题就是如何确定顶点间是否已“连通”?因此,我们应该考虑到来存储顶点的连通信息。如果两个及以上的顶点连在了一起,则它们的连通信息应该是相同的。那么为此,我们可以设置一个一维数组来存储它们的连通信息,即该值的初始化应是(1,2,3,…)这样不同的序号来代表它们最开始都是单独的一个连通分量。若相同,则改变这个值就行了。
下面为两个数组的存储表示:
struct ED
{
VerTexType Head; //边的起点
VerTexType Tail; //边的终点
ArcType lowcost; //边的权值
}Edge[arcnum];
int Vexset[MVNum]; //顶点的连通信息
算法分析:
- 将权值按从小到大的顺序排列,可以用一个循环嵌套一个循环的方式来实现
- 接下来就是加边,这里用一个循环来完成(注意,边的数量应等于无向网的中边的数量,这个算法是在不断地筛选出适合构造最小生成树的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),且与网中的边的数目无关,因此该算法适用于稠密网。对于克鲁斯卡尔算法,它的时间复杂度与边的数目有关,所以克鲁斯卡尔算法更适用于稀疏网。