当前位置: 首页 > news >正文

01数据结构-Kruskal算法

01数据结构-Kruskal算法

  • 1.最小生成树
    • 1.1生成树的定义
    • 1.2生成树的属性
    • 1.3最小生成树
  • 2.克鲁斯卡尔(Kruskal)算法
  • 3.Kruskal算法代码实现

1.最小生成树

1.1生成树的定义

在含有n个顶点的连通图中选择n-1条边,构成一颗极小的连通子图,并使该连通子图中n-1条边上的权值之和达到最小,则称其为连通网的最小生成树

一个连通网的生成树是一个极小的连通子图,它包含图中全部的n个顶点,但只有构成一棵树的n-1条边

连通图和它对应的生成树,可以用于解决实际生活中的问题:假设A,B,C和D4座城市,为了方便生产生活,要为这4座城市建立通信,对于4个城市来讲,本着节约经费的原则,只需要建立3个通信路线即可,如图1右边。在具体选择哪一种方式的时候,需要综合考虑城市之间间隔的距离,建设通信线路的难度等各种因素,将这些因素综合起来用一个数值表示,当作这条路的权值。
图1
图1

1.2生成树的属性

  • 一个连通图可以有多个生成树
  • 一个连通图的所有生成树都包含相同的顶点个数和边数
  • 生成树中不存在环
    • 生成树是一棵树(Tree),即不存在环路(无闭合路径),并且所有顶点通过边相互连通。
  • 移除生成树中的任意一边都会导致图的不连通,生成树的边最少的特性
  • 在生成树中添加一条边会构成环,对于包含n个顶点的连通图,生成树包含n个顶点和n-1条边
  • 对于包含n个顶点的无向图最多包含nn-2棵生成树

1.3最小生成树

所谓一个带权图的最小生成树,就是原图中的边的权值最小的生成树,最小是指边的权值之和小于或者等于其他生成树的边的权值之和。

最小生成树的算法思想是从顶点集或边集的角度进行贪婪化。

2.克鲁斯卡尔(Kruskal)算法

克鲁斯卡尔算法(Kruskal)是⼀种使⽤贪婪⽅法的最⼩⽣成树算法。该算法初始将图视为森林,图中的每⼀个顶点视为⼀棵单独的树。⼀棵树只与它的邻接顶点中权值最⼩且不违反最⼩⽣成树属性(不构成环)的树之间建⽴连边。

如图2,是一个带权图,我们要找到最小生成树,一共7个顶点我们需要从这么多边中选出7-1=6条边来构成我们的生成树,我们采用贪心的思想,只要是权值小的边我们统统加入生成树,但是需要有一个约束,因为生成树中不能有环,所以我们的约束条件是每加一个权值小的边,就去看是否与之前已经加上的边构成了一个环,如果没有我们就加入这条边。

图2
图2

如图3我们采用贪心,在所有边中找到权值最小的边是2,这条边连接的是F和E,由于是第一条边所以直接连,这样F和E就连通了,我们认为F和E在同一个集合中,再找权值最小的边是3,这条边连接的是C和D,和之前的FE集合不在一起,所以可以直接连接,这样C和D就连通了,我们认为CD在同一集合中,再找权值最小的边是4,这条边连接的是D和E,在连接这条边之前,CD集合和FE集合各是独立的,所以连接了这条边后没问题,连接这条边把FE集合和CD集合融在同一集合中,再找权值最小的边是5,这条边连接的是C和E,在连接这条边之前,CD集合和FE集合已经是在同一集合中了,即C和E肯定有间接连接的,所以这条边是不能连接的。

如何判断某条边加入后是否会形成环:
A点 边 B边,A点和B点已经有间接路径(在同一集合中)了,这个时候再连肯定就会出现环了。

我们之前学过的一个数据结构叫做并查集,就是专门来干这件事的,判断两个顶点在不在同一集合中和Union(a,b)。我们采用quickUnion的思想
图3
图3

如图4,很明显权值为6的这条边也不能连接,再找下一个7,F的父节点是C,B的父节点是B,两者不同,所以可以连接F和B,再找8,连接G和E,再找12,连接A和B。最后这个最小生成树的权值为36。

图4
图4

我们知道5种图的存储方式:邻接矩阵,邻接表,十字链表,多重邻接表,边集数组,我们暂时学过前面4种,那用哪一种方式来存这个带权图呢?

回想一下我们对这个带权图的操作,我们是在找图中的边的最小值,意思是我们就存边,把边的权值从小到大排个序,在满足条件的情况下,一直找边的权值的最小值,咋一看这里应该使用边集数组来存这个带权图,但是又有一个问题,边集数组只存了边,我们的顶点在哪里存呢?不适用通用性。所以边集数组只是一个临时空间,我们可以先用邻接矩阵表示通用图,这个通用图我们把它转成边集数组,有了边集数组我们再实现Kruskal算法,来寻找最小生成树。

3.Kruskal算法代码实现

头文件定义:

我们需要用到之前写的邻接矩阵的结构,common里面写的是边集数组的结构,由于后面Prim算法也会用到这个,所以干脆把这个结构体封装成一个文件了。

#ifndef COMMON_H
#define COMMON_H
/*边集数组的结构*/
typedef struct  {int begin;      //边的起点 (顶点1)int end;        //边的终点 (顶点2)int weight;     //边的权值
}EdgeSet;
#endif //COMMON_H

在Kruskal头文件里写三个接口函数,分别是用来从邻接矩阵中初始化边集数组,排序边集数组和实现Kruskal算法。

#ifndef KRUSKAL_H
#define KRUSKAL_H
/* 利用邻接矩阵 生成一个边集数组,利用边集数组实现Kruskal算法* 算法思路:* a. 将所有的边按权值的大小进行升序排序,从小到大依次提取边的情况,满足条件* b. 加入这条边,和之前已经选中的边不能组成环路,如果不能,这条边就归入生成树的结果部分* c. 如果能,舍去,再找下一条,直到筛选出n - 1条边*/
#include"matrixGraph.h"
#include"common.h"// 从邻接矩阵中初始化边集数组,返回值表示边集数组的个数
void initEdgeSet(const MatrixGraph *graph, EdgeSet *edges);// 排序边集数组
void sortEdgeSet(EdgeSet *edges, int num);// Kruskal算法
int KruskalMGraph(const EdgeSet *edges, int num, int node_num, EdgeSet *result);
#endif //KRUSKAL_H

初始化边集数组:void initEdgeSet(const MatrixGraph *graph, EdgeSet *edges);

void initEdgeSet(const MatrixGraph* graph, EdgeSet* edges) {int k = 0;for (int i = 0; i < graph->nodeNum; ++i) {				// 遍历每个顶点for (int j = i + 1; j < graph->nodeNum; ++j) {		// 遍历上三角(无向图所以只需遍历一半)if (graph->edges[i][j] > 0) {edges[k].begin = i;edges[k].end = j;edges[k].weight = graph->edges[i][j];k++;}}}
}

这个接口是用原来邻接矩阵存的有权图来初始化边集数组Edge,因为是无向图,邻接矩阵的边的集合是对称矩阵,我们只需要遍历一半就行。

排序:void sortEdgeSet(EdgeSet* edges, int num);

void sortEdgeSet(EdgeSet* edges, int num) {EdgeSet tmp;for (int i = 0; i < num; ++i) {for (int j = i + 1; j < num; ++j) {if (edges[j].weight < edges[i].weight) {memcpy(&tmp, &edges[i], sizeof(EdgeSet));memcpy(&edges[i], &edges[j], sizeof(EdgeSet));memcpy(&edges[j], &tmp, sizeof(EdgeSet));}}}
}

将边集数组中的边的权值按从小到大排好。

先来简单测试下我们这两个接口的正确性:

#include <stdlib.h>#include"Kruskal.h"
#include"stdio.h"void setUpMatrixGraph(MatrixGraph *graph) {//				  0    1    2    3    4    5   6char *names[] = {"A", "B", "C", "D", "E", "F", "G"};initMatrixGraph(graph, names, sizeof(names)/sizeof(names[0]), 0, 0);addMGraphEdge(graph, 0, 1, 12);addMGraphEdge(graph, 0, 5, 16);addMGraphEdge(graph, 0, 6, 14);addMGraphEdge(graph, 1, 2, 10);addMGraphEdge(graph, 1, 5, 7);addMGraphEdge(graph, 2, 3, 3);addMGraphEdge(graph, 2, 4, 5);addMGraphEdge(graph, 2, 5, 6);addMGraphEdge(graph, 3, 4, 4);addMGraphEdge(graph, 4, 5, 2);addMGraphEdge(graph, 4, 6, 8);addMGraphEdge(graph, 5, 6, 9);
}void showEdges(EdgeSet *edges,int num) {for (int i = 0; i < num; ++i) {printf("<%d>---<%d>---<%d>\n",edges[i].begin,edges[i].end,edges[i].weight);}
}void test01() {MatrixGraph graph;EdgeSet *edges;setUpMatrixGraph(&graph);edges=malloc(sizeof(EdgeSet)*graph.edgeNum);if (edges==NULL) {printf("malloc error");return;}initEdgeSet(&graph,edges);sortEdgeSet(edges,graph.edgeNum);showEdges(edges,graph.edgeNum);free(edges);
}int main() {test01();return 0;
}

我们以图2举例,void setUpMatrixGraph(MatrixGraph *graph)这个函数就是在初始化图2的这个带权图的过程,在test01中调用void setUpMatrixGraph(MatrixGraph *graph)初始化好后,动态申请一个边集数组,调用Kruskal.h中写的初始化函数:void initEdgeSet(const MatrixGraph *graph, EdgeSet *edges);再调用写的排序函数void sortEdgeSet(EdgeSet * edges, int num);最后打印出结果如下:

D:\work\DataStruct\cmake-build-debug\03_GraphStruct\MiniTree.exe
<4>---<5>---<2>
<2>---<3>---<3>
<3>---<4>---<4>
<2>---<4>---<5>
<2>---<5>---<6>
<1>---<5>---<7>
<4>---<6>---<8>
<5>---<6>---<9>
<1>---<2>---<10>
<0>---<1>---<12>
<0>---<6>---<14>
<0>---<5>---<16>进程已结束,退出代码为 0

可以看到第三列的权值是从小到大排列的。

Kruskal算法:int KruskalMGraph(const EdgeSet* edges, int num, int node_num, EdgeSet* result);

static int getRoot(const int *uSet, int a) {while (uSet[a] != a) {a = uSet[a];}return a;
}int KruskalMGraph(const EdgeSet* edges, int num, int node_num, EdgeSet* result) {int *uSet;int count = 0;int sum = 0;// 1. 初始化并查集uSet = malloc(sizeof(int) * node_num);if (uSet == NULL) {printf("malloc failed\n");return -1;}for (int i = 0; i < node_num; ++i) {uSet[i] = i;}// 2. 从已经排序好的边集中,找到最小的边,不构成环for (int i = 0; i < num; ++i) {int a = getRoot(uSet, edges[i].begin);int b = getRoot(uSet, edges[i].end);if (a != b) {// 不构成环uSet[a] = b;result[count].begin = edges[i].begin;result[count].end = edges[i].end;result[count].weight = edges[i].weight;sum += edges[i].weight;count++;if (count == node_num - 1) {break;}}}free(uSet);return sum;
}

先申请并查集的的空间,然后初始化它,初始时并查集中的每一个顶点的父节点都指向的是自己,然后开始贪心,如果两个集合的root不一样,把一个集合的root交给另一个集合,即合并集合,一直循环到count(边数)等于顶点-1,最小生成树已构建完成,free掉并查集。

最后来测一下:

#include <stdlib.h>#include"Kruskal.h"
#include"stdio.h"void setUpMatrixGraph(MatrixGraph *graph) {//				  0    1    2    3    4    5   6char *names[] = {"A", "B", "C", "D", "E", "F", "G"};initMatrixGraph(graph, names, sizeof(names)/sizeof(names[0]), 0, 0);addMGraphEdge(graph, 0, 1, 12);addMGraphEdge(graph, 0, 5, 16);addMGraphEdge(graph, 0, 6, 14);addMGraphEdge(graph, 1, 2, 10);addMGraphEdge(graph, 1, 5, 7);addMGraphEdge(graph, 2, 3, 3);addMGraphEdge(graph, 2, 4, 5);addMGraphEdge(graph, 2, 5, 6);addMGraphEdge(graph, 3, 4, 4);addMGraphEdge(graph, 4, 5, 2);addMGraphEdge(graph, 4, 6, 8);addMGraphEdge(graph, 5, 6, 9);
}// void showEdges(EdgeSet *edges, int num) {
//     for (int i = 0; i < num; ++i) {
//         printf("<%d>---<%d>---<%d>\n", edges[i].begin, edges[i].end, edges[i].weight);
//     }
// }void test01() {MatrixGraph graph;EdgeSet *edges;EdgeSet *result;setUpMatrixGraph(&graph);edges=malloc(sizeof(EdgeSet)*graph.edgeNum);if (edges==NULL) {printf("malloc error");return;}initEdgeSet(&graph,edges);sortEdgeSet(edges,graph.edgeNum);//showEdges(edges,graph.edgeNum);result = malloc(sizeof(EdgeSet) * (graph.nodeNum - 1));if (result == NULL) {free(edges);return;}int sum = KruskalMGraph(edges, graph.edgeNum, graph.nodeNum, result);for (int i = 0; i < graph.nodeNum - 1; i++) {printf("edge %d: <%s> --- <%d> ---- <%s>\n", i + 1,graph.vex[result[i].begin].show, result[i].weight, graph.vex[result[i].end].show);}printf("sum = %d\n", sum);free(result);free(edges);
}int main() {test01();return 0;
}

由于边集数组中没有顶点,要想打出顶点,要用 graph.vex(顶点集)下的result[i].begin(下标)下的显示行为。

结果:

D:\work\DataStruct\cmake-build-debug\03_GraphStruct\MiniTree.exe
edge 1: <E> --- <2> ---- <F>
edge 2: <C> --- <3> ---- <D>
edge 3: <D> --- <4> ---- <E>
edge 4: <B> --- <7> ---- <F>
edge 5: <E> --- <8> ---- <G>
edge 6: <A> --- <12> ---- <B>
sum = 36进程已结束,退出代码为 0

大概先写这些吧,今天的博客就先写到这,谢谢您的观看。

http://www.dtcms.com/a/328283.html

相关文章:

  • 破译真实感:渲染参数进阶指南——告别塑料感,唤醒材质生命力
  • 01. maven的下载与配置
  • ubuntu24下keychorn键盘连接不了的改建页面的问题修复
  • “生成式UI革命”:Tambo AI如何让你的应用“开口说话、动手搭界面” | 全面深剖、案例实践与未来展望
  • Seed-VC:零样本语音转换与扩散transformer
  • 08--深入解析C++ list:高效操作与实现原理
  • 从爬虫新手到DrissionPage实践者的技术旅程
  • 【IP查询】使用IP66(ip66.net)验证IP地址定位的准确率
  • 小智智能交互算法通过国家备案,视觉大模型引领AI应用新浪潮
  • 机器学习之TF-IDF文本关键词提取
  • 终端安全检测与防御技术
  • 数据结构:中缀到后缀的转换(Infix to Postfix Conversion)
  • 【速通版!语义通信基础与前沿学习计划】
  • C++中类之间的关系详解
  • AR巡检:三大核心技术保障数据准确性
  • Langchain入门:构建一个PDF摄取和问答系统
  • 51 单片机分层架构的模块依赖关系图
  • 解决ROS编译顺序不对,需要内部依赖,因此要多次编译的问题
  • Python初学者笔记第二十二期 -- (JSON数据解析)
  • MySQL 数据库表操作与查询实战案例
  • 双十一美妆数据分析:洞察消费趋势与行业秘密
  • 机械臂的智能升维:当传统机械臂遇见Deepoc具身智能大模型从自动化工具到具身智能体的范式革命
  • Element用法---Loading 加载
  • C++的异常的使用和规范
  • 【盘古100Pro+开发板实验例程】FPGA学习 | 均值滤波 | 图像实验指导手册
  • 【代码随想录day 18】 力扣 501.二叉搜索树中的众数
  • 免费播客翻译与转录:用中文收听全球播客
  • Langchain入门:文本摘要
  • C++学习之数据结构:AVL树
  • java八股文-MySql面试题-参考回答