//最小生成树算法:普利姆算法和克鲁斯卡尔算法
/*
构成最小生成树的算法有很多,但是其根本原则就两条:
1、尽可能的选择权值最小的边,但是不能构成回路
2、根据顶点个数n,选择n-1条边构成树
*/
/*
普利姆算法构成最小生成树的过程中:需要建立两个数组Vset[]和Lowcost[]。Vset[i] == 1表示顶点i已经被并入生成树
中了,Vset[i] == 0,表示顶点i还未被并入生成树中。Lowcost[]数组中存放的是当前生成树到剩余的还未被并入生成树
的各顶点最短边的权值。对“Lowcost[]数组中存放的是当前生成树到剩余的还未被并入生成树的各顶点最短边的权值”的
理解:
1、这句话的意思是生成树这一个整体到其余顶点的权值,而不是针对树中的某一顶点
2、当前生成树到某一顶点(树外的顶点)可能有多条边。例如,对于顶点i,Lowcost[i]保存的是当前生成树到顶点i
的多条边中最短的一条边的权值。Lowcost[i]中有多个最短边的权值,最短边条数对应于剩余顶点个数,已经并入生成
树的边不在考虑范围之内。
普利姆算法执行过程:从图中某一个顶点v开始,构造生成树的算法执行过程如下:
1、将v到其他顶点的所有边当做候选边;
2、重复一下步骤n-1次,使得n-1个顶点全部被并入到生成树中:
1)从候选边中挑选出权值最小的边输出,并将与该边另一端相连的顶点V1并入生成树中;
2)考察所有的剩余顶点Vi (此时i>=2),如果(V1,Vi)的权值比Lowcost[Vi]小,则用(V1,Vi)的权值更新
Lowcost[Vi]。
普利姆算法代码如下:
*/
void Prim(MGraph &G, int V0, int &sum) //以图的邻接矩阵存储方式为例
{
int Lowcost[MAXSIZE] = { 0 }, Vset[MAXSIZE] = { 0 }, vtemp = 0;// vtemp用来存储循环过程中的最新并入生成树中的顶点
int i = 0, j = 0, k =0,min = 0;
vtemp = V0;
for (i = 0; i < G.n; ++i)
{
Lowcost[i] = G.edges[V0][i]; //给Lowcost[]数组中的i进行赋值,此时Lowcost[i]表示的是V0到顶点i的边的权值
Vset[i] = 0;
}
Vset[V0] = 1; //将顶点v并入生成树中
sum = 0;
for (i = 0; i < G.n; ++i)
{
min = INT8_MAX;//INT8_MAX是一个已经已定义的常量,其值比图中所有边的权值都大
for( j = 0 ;j < G.n ; ++j ) //这个循环用于选出候选边中的最小者
if (0 == Vset[j] && Lowcost[j] < min) //选出当前生成树到其余顶点最短边中的最短的一条(注意此处两个最短的含义)
{
min = Lowcost[j];
k = j;
}
Vset[k] = 1; //将顶点Vk并入生成树中
vtemp = k;
sum += min; //sum记录的是最小生成树的权值
for ( j = 0 ; j < G.n ; ++j )//这个循环以刚并入的顶点v为媒介更新候选边
{
if ( 0 == Vset[j] && G.edges[vtemp][j] < Lowcost[j]) //此处对应算法
{
Lowcost[j] = G.edges[vtemp][j]; //执行过程中的第二步
}
}
}
}
/************************************************************************/
/*普利姆算法的时间复杂度为O(n*n)。普利姆算法主要部分是一个双重循环,外层循环
内有两个并列的单层讯循环,单层循环内的操作都是常量级别的,因此可以取任意一个
单层循环内的操作为基本操作。取其执行次数为n*n。普利姆算法的时间复杂度至于图中
顶点数有关系,与边数没有关系。因此普利姆算法适用于稠密图*/
/*克鲁斯卡尔算法:
思想:每次找出候选边中权值最小的边,就将该边并入生成树中。重复此过程直到所有边都被检测完为止
执行过程:将图中的边按权值从小到大进行排序,然后从权值最小的边开始扫描各条边,并检测当前边是
否为候选边,即是否该边的并入会构成回路:如果不构成回路则将该边并入到生成树中,直到所有边都被
检测完为止。
判断是否会产生回路需要用到并查集。并查集保存了一棵树或者几棵树(森林),这些树有这样的特点:
通过树中一个结点可以找到其双亲结点,进而找到根结点(其实就是树的双亲存储结构)。这种特
性有两个好处:一是可以快速的将两个含有很多元素的集合 合并为一个。两个集合就是并查集中的两棵树
,只需要找到其中一棵树的根,然后将其作为另一颗树中任何一个节点的孩子结点即可。二是可以方便的判
断两个元素是否属于同一个集合。通过这两个元素所在的结点找到它们的根结点,如果它们有相同的根,说
明它们属于同一个集合,否则属于不同的集合。并查集可以简单地用一维数组来表示。 假设road[]数组中已经存放了图中各边即所连接的两个顶点的信息,排序函数已经存在,则克鲁斯卡尔算法的
代码如下:
*/
typedef struct Road
{
int a, b; //a和b为一条边所连接的两个顶点
int w; //边的权值
}Road;
Road road[MAXSIZE];
int V[MAXSIZE]; //定义并查集数组
int getRoot(int k) //在并查集中查找根结点的函数
{
while (k != V[k])
k = V[k]; //并查集中只有根结点才满足 k == V[k]
return k;
}
void KusKal(MGraph G, int &sum, Road road[]) //克鲁斯卡尔算法
{
int i = 0;
int N, E, a, b;
N = G.n;
E = G.e;
sum = 0;
for (i = 0; i < N; ++i)
V[i] = i; //初始时每个顶点构成一个独自的并查集,n个顶点即有n个并查集
/*sort <Road>( &road, E, sizeof(road), cmp);*/
qsort(&road, E, sizeof(road),
[](void const* t1, void const* t2)->int //lambda表达式,返回类型一定要是int,bool类型会报错,我也不知道为什么,绝望ing
{ //参数列表一定要是void const*,不能是Road const*,否则还是会报错。此处要不要const都无所谓
return (*(Road*)t1).w - (*(Road*)t2).w; //在这里将t1,t2转换为Road*指针类型,然后根据边值进行排序
}
); //C++中的快排算法,对road数组中的E条边按其权值从小到大进行排序
for (i = 0 ; i < E ;++i)
{
a = getRoot( road[i].a);
b = getRoot( road[i].b);
if (a != b)
{
V[a] = b;
sum += road[i].w; //求生成树的权值,此句并不是算法的固定写法,也可以从换成其他的,例如将生成树的各边输出或者放在数组里面
}
}
}