最小生成树:一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边
最小生成树可以使用最少成本连接各个地点,例如:铺放线路、修建道路等
无向连通图转化为最小生成树的示例图如下:
最小生成树可以用克鲁斯卡尔(kruskal)算法或普里姆(prim)算法求出
此两个算法实现过程,一个通过增加顶点不停更新最短边,一个增加边不停更新顶点分组,均应用到了动态规划逻辑(即,子分支不独立,需要依赖上一次的结果来执行,一步一步得出最终结果),顺道提一下分治法(即:一个大的步骤可以分成若干个子分支,且只要每个子分支的解求得,即可求出该大步骤的解,一般以递归的方式求解,例如:快速排序、归并排序)
普里姆算法(prim)
prim算法通过不停新增顶点方式生成最小生成树,以此同时每轮都会更新最短边,因此prim算法又称为加点法
案例流程
此算法是通过加入顶点的过程,不停的更新剩余边为最小距离的边,最终生成最小生成树
下面通过案例一步一步生成最小生成树,以此阐述prim算法的执行流程
1.由于每个顶点都会参入,所以随机选择一个顶点先进入集合N(new)中,其余顶点在集合S(中),如下图所示,选出顶点A
1.然后从A到各个顶点中选出距离最短(A-D)的一个顶点D,将D加入集合N中,并且更新New中顶点距离其他顶点的最短距离,且N中顶点距离其他同一个顶点的距离取最短的保留
3.接着从集合N中(此时是A、D),找出距离S(B、C、F)中顶点距离最近的顶点F,到达剩余顶点S中最短边为D-F,放入到N集合中,并且更新到剩余顶点的距离,保留最短距离边
4.接着从集合N中(此时是A、D、F),找出距离S(B、C)中顶点距离最近的顶点B,到达剩余顶点S中最短边为D-B, 放入到集合N中,并且更新剩余顶点距离,保留最短距离边(此时发现已经和结果一样了,算是巧合,还有最后一步筛选)
5.最后从集合N中(此时是A、D、F、B),找出距离S(C)中顶点距离最新的顶点B,到达剩余顶点S中最短边为A-C,此时行程最小生成树,结束
代码实现
代码实现的最小生成树结果,采用边集数组来保存
1.定义一下边集数组结构和连通图数据
即:储存最小生成树结果的边集数组以及邻接表集合(存放各个顶点之间的连接关系,也就是当前连通图)
//边集数组内容基本结构
typedef struct {
int fromvex;
int tovex;
int weight;
} LSLinesNode;
//初始化一个邻接矩阵,用于保存节点位置
static int linesList[N][N] = {
{0 , 6, MAXINT, MAXINT, 11, 14},
{6 , 0, 21, MAXINT, MAXINT, 8},
{MAXINT, 21, 0, 15, MAXINT, 18},
{MAXINT, MAXINT, 15, 0, 12, MAXINT},
{11, MAXINT, MAXINT, 12, 0, 16},
{14, 8, 18, MAXINT, 16, 0},
};
2.实现代码如下:
参考案例流程,每次加入一个点的过程都会把集合N到集合S中的其他路径更新,因此实现逻辑如下:
1.声明结果边集数组集合用于保存最小生成树的边
2.先默认第一个点到其他所有顶点的边为最短边集合,放入结果边集数组(即可看做第一个点先加入集合N,其他顶点在集合S中),然后通过后续更新边集来更新结果
3.从第一条边开始,遍历所有顶点,寻找最短路径的同时更新边集数组
4.找出第i条边(集合N)和到其他顶点连接(集合S)的最短边,每次加入一个新顶点,则认为改顶点加入集合N,否则在集合S中
5.将最小边加入到结果边集数组的对应位置,其位置与i相同(改变加入的新顶点,进入集合N中,S中移除该顶点)
6.与此同时,将其和S集合中的顶点所有连接的边和先前所加入的到其他顶点的最短边进行对比,如果比先前的边短,则全部替换更新,否则保留
7.顶点i递增,进入下一轮,重复步骤4
这样更新出来的边集数组,即为最小生成树的集合了。
//普里姆算法(默认系统使用邻接矩阵保存的)
void generateMinTreeByPrim() {
printf("prim算法:加点法\n");
//默认给予某一个节点到其他顶点连接为最小生成树, 通过不停更新新顶点,来生成最小生成树
int n = N - 1;
LSLinesNode nodes[n];
//将第一个点相关的所有边都放入边集数组
for (int i = 0; i < n - 1; i++) {
nodes[i].fromvex = 0;
nodes[i].tovex = i + 1;
nodes[i].weight = linesList[0][i + 1];
}
for (int i = 0; i < n; i++) {
//找出最小的边安排到最前面
int minIndex = i;
int j = i + 1;
for (; j < N; j++) {
if (nodes[j].weight < nodes[minIndex].weight) {
minIndex = j;
}
}
//最小的边筛选出来,换到前面去,像排序一样
if (minIndex != i) {
LSLinesNode node = nodes[minIndex];
nodes[minIndex] = nodes[i];
nodes[i] = node;
}
//终点均为剩下的顶点,所以只需要当前节点与终点的边的权值与剩余对应的边权值比较
//较小的替换进边集数组即可保证内部均为最小边
for (j = i + 1; j < N; j++) {
//默认邻接矩阵
int weight = linesList[nodes[i].tovex][nodes[j].tovex];
if (weight < nodes[j].weight) {
nodes[j].fromvex = nodes[i].tovex; //由于终点节点一致,只需要替换权值和初始节点即可
nodes[j].weight = weight;
}
}
}
}
克鲁斯卡尔(kruskal)
kruskal算法是通过一直增加边的方法来生成最小生成树,以此同时不停更新顶点分组,因此kruskal算法又被成为加边法
案例流程
此算法形成最小生成树的逻辑是通过分组的方式,不停地通过添加合并组的方式,累计加入n-1条边,最终生成最小生成树
下面通过案例一步一步生成最小生成树,以此阐述kruskal算法的执行流程
1.先将其所有边加入边集数组集合中,然后对比按从小到大进行排序,由于kruskal又称加边法,需要从最小的边开始依次加入,还将每个顶点各分为一组,每组为其对应的号码(A、B、C、D、F分别为0、1、2、3、4)
2.从边集数组加入第1条边,对比其首尾顶点是否在同一个组,发现一个在0组(A)一个在3组(D),将两个顶点标记为一组,这里将A-D边的后者(D)所在组归并为前者(A)所在组(0组),且标记已加入1条边(这案例只需要加入n-1条边,即4条边即可生成最小生成树)
3.从边集数组加入第2条边,对比其首尾顶点是否在同一个组,发现一个在0组(D)一个在4组(F),将两个顶点标记为一组,这里将D-F边的后者(F)所在组归并为前者(D)所在组(0组),且标记已加入2条边
3.从边集数组加入第3条边,对比其首尾顶点是否在同一个组,发现一个在0组(B)一个在4组(D),将两个顶点标记为一组,这里将B-D边的后者(D)所在组归并为前者(B)所在组(1组),且标记已加入3条边
4.从边集数组加入第4条边,对比其首尾顶点是否在同一个组,发现一个在4组(A)一个在4组(C),将两个顶点标记为一组,这里将A-C边的后者(C)所在组归并为前者(A)所在组(1组),且标记已加入4条边,此时结果边集数组4条边了,已经形成最小生成树,结束
注:此案例覆盖并不全(没有出现同组的情况),加新边时需要注意,需要检测两个边的顶点是否在同一个组(没有加入过的一定不会在同一个组,只有两个都加入过的才会在同一个组)
代码实现
代码实现的最小生成树结果,采用边集数组来保存
1.定义一下边集数组结构和连通图数据
即:储存最小生成树结果的边集数组以及邻接表集合(存放各个顶点之间的连接关系,也就是当前连通图)
//边集数组内容基本结构
typedef struct {
int fromvex;
int tovex;
int weight;
} LSLinesNode;
//初始化一个邻接矩阵,用于保存节点位置
static int linesList[N][N] = {
{0 , 6, MAXINT, MAXINT, 11, 14},
{6 , 0, 21, MAXINT, MAXINT, 8},
{MAXINT, 21, 0, 15, MAXINT, 18},
{MAXINT, MAXINT, 15, 0, 12, MAXINT},
{11, MAXINT, MAXINT, 12, 0, 16},
{14, 8, 18, MAXINT, 16, 0},
};
//保存所有边集数组集合,如果不存在则从邻接矩阵内获取,后面逻辑默认已经有了所有边行程的边集数组
LSLinesNode nodes[count];
2.实现代码如下:
参考案例流程,每次加入一个条边的过程,都会更新顶点所在组,实现逻辑如下:
1.对所有边集数组nodes进行从小到大排序
2.把所有顶点各自分为一组表示,声明保存最小生成树结果的边集数组result,以及已经获取的最小生成树的边数
3.遍历所有边集数组nodes,对里面的所有边进行如下处理
4.如果此边两个顶点不在一个组,则将后节点放置到前节点所在组;否则,重复步骤四
5.分组处理完毕后,查看是否已经获取到了最小生成树所需数量的边数,如果已经满足,结束循环最小生成树已形成;否则,重复步骤4,继续生成
//如果只有邻接矩阵则转化为边集数组
void getAllLineNodes(LSLinesNode *nodes, int *count) {
*count = getLinesCount(linesList);
//生成有值的边集数组
int index = 0;
for (int i = 0; i < N; i++) {
for (int j = i + 1; j < N; j++) {
if (linesList[i][j] > 0 && linesList[i][j] < MAXINT) {
nodes[index].fromvex = i;
nodes[index].tovex = j;
nodes[index].weight = linesList[i][j];
index++;
}
}
}
}
//克鲁斯卡尔(这里采用边集数组保存所有边来进行处理)
void generateMinTreeByKruskal(LSLinesNode *nodes, int count) {
printf("kruskal算法:加边法\n");
int count = 0;
LSLinesNode nodes[count];
getAllLineNodes(nodes, &count); //生成所有边的边集数组
//先升序排序,然后从最小的一个个挑出n-1个没有闭环的最小生成树
for (int i = 0; i < count; i++) {
for (int j = i; j < count; j++) {
if (nodes[i].weight > nodes[j].weight) {
LSLinesNode node = nodes[i];
nodes[i] = nodes[j];
nodes[j] = node;
}
}
}
int *vertexs[N]; //顶点数组,表示被分的组,默认一人一组,保存指针方便修改分组结果一致
for (int i = 0; i < N; i++) {
*(vertexs[i]) = i;
}
LSLinesNode result[N-1]; //保存最终结果的边集数组
int num = 0;
for (int i = 0; i < count; i++) {
LSLinesNode node = nodes[i];
//如果两个点不在一个组
if (vertexs[node.fromvex] != vertexs[node.tovex]) {
result[num] = node;
num++;
*(vertexs[node.tovex]) = *(vertexs[node.fromvex]); //更改指针内容指向同一组
if (num >= N-1) break;
}
}
}