Kruskal 算法
文章目录
- [Kruskal 算法](#Kruskal 算法)
-
- [1. 生成树](#1. 生成树)
-
- [1.1 生成树的定义](#1.1 生成树的定义)
- [1.2 生成树的属性](#1.2 生成树的属性)
- [2. 最小生成树](#2. 最小生成树)
- [3. Kruskal 算法](#3. Kruskal 算法)
-
- [3.1 简介](#3.1 简介)
- [3.2 举例](#3.2 举例)
- [4. 边集数组](#4. 边集数组)
- [5. 代码实现](#5. 代码实现)
-
- [5.0 前置 邻接矩阵](#5.0 前置 邻接矩阵)
- [5.1 头文件部分](#5.1 头文件部分)
- [5.2 函数实现](#5.2 函数实现)
-
- [5.2.1 从邻接矩阵中初始化边集数组](#5.2.1 从邻接矩阵中初始化边集数组)
- [5.2.2 排序边集数组 (选择排序)](#5.2.2 排序边集数组 (选择排序))
- [5.2.3 Kruskal算法](#5.2.3 Kruskal算法)
- [5.3 main 函数](#5.3 main 函数)
- [5.4 输出结果](#5.4 输出结果)
1. 生成树
1.1 生成树的定义
- 对于有n个顶点的连通图,至少有n-1条边,那么连接所有顶点的极小连通子图就是生成树。如果在图的生成树种任意加一条边,则必然生成回路

1.2 生成树的属性
-
⼀个连通图可以有多个⽣成树;
-
⼀个连通图的所有⽣成树都包含相同的顶点个数和边数;
-
⽣成树当中不存在环;
-
移除⽣成树中的任意⼀条边都会导致图的不连通, ⽣成树的边最少特性;
-
在⽣成树中添加⼀条边会构成环。
-
对于包含n个顶点的连通图,⽣成树包含n个顶点和n-1条边;
-
对于包含n个顶点的⽆向完全图最多包含 n^n-2颗⽣成树。
2. 最小生成树
-
对于连通网来说,边是带权值的,生成树的各边也带权值,因此把生成树各边的权值总和称为生成树的权,把权值最小的生成树称为最小生成树
-
举个例子



-
上图中,原来的带权图可以⽣成左侧的两个最⼩⽣成树,这两颗最⼩⽣成树的权值之和最⼩,且
包含原图中的所有顶点
-
我们⼜如何从原图得到最⼩⽣成树呢?
-
最⼩⽣成树算法有很多,其中最经典的就是 克鲁斯卡尔(Kruskal)算法 和 普⾥姆(Prim)算法,也是我们考试、⾯试当中经常遇到的两个算法。
-
下面来写 克鲁斯卡尔(Kruskal)算法 , **普⾥姆(Prim)**在下节来写
3. Kruskal 算法
3.1 简介
-
克鲁斯卡尔算法(Kruskal)是⼀种使⽤贪婪⽅法(贪心算法)的最⼩⽣成树算法。该算法初始将图视为森林,图中的每⼀个顶点视为⼀棵单独的树。⼀棵树只与它的邻接顶点中权值最⼩且不违反最⼩⽣成树属性(不构成环)的树之间建⽴连边。
-
该算法的基本思想是从小到大加入边,是个贪心算法。在加入边时,需要通过 并查集 来判断该边的两个端点是否属于同一个集合 (树),避免形成 "环"
3.2 举例

- 第⼀步:将图中所有的边按照权值进⾏⾮降序排列

-
第⼆步:从图中所有的边中选择可以构成最⼩⽣成树的边
-
1、选择权值最⼩的边 V4-V7 ,没有环形成,则添加:

- 2、选择边 V2-V8,没有形成环,则添加

- 3、选择边V0 - V1没有形成环,则添加

- 4、选择边V0 - V5,没有形成环,添加

- 5、选择边V1 - V8,没有形成环,添加

- 6、选择V3 - V7,没有形成环,添加

- 7、选择V1 - V6,没有形成环,添加

- 8、选择边V5 - V6添加这条边将导致形成环,舍弃,不添加
- 9、选择边V1 - V2添加这条边将导致形成环,舍弃,不添加
- 10、选择边V6 - V7,没有成环,添加

- 此时已经包含了图中顶点个数9减1条边,也就是8条边,算法停⽌
4. 边集数组
- 边集数组是由两个⼀维数组构成,⼀个是存储顶点的信息,另⼀个是存储边的信息,这个边数组每个数据元素由⼀条边的起点下标(begin)、终点下标(end)和权(weight)组成。

- 具体看下面代码实现吧
5. 代码实现
5.0 前置 邻接矩阵
- 由于要用到邻接矩阵的内容 这里贴一下我们之前写过的部分
c
//邻接矩阵的顶点结构 表示一个顶点的信息
typedef struct
{
int no; //顶点编号
char* show; //显示行为
} MatrixVertex;
typedef int MatrixEdge; //边里面存的内容
//图邻接矩阵表示法
typedef struct
{
MatrixVertex vex[MaxNodeNum]; //存储顶点信息,使用一维数组存储顶点
int nodeNum; //约束实际顶点数量
MatrixEdge edges[MaxNodeNum][MaxNodeNum]; //边的集合 //存储边的结构,矩阵
int edgeNum; //边的数量
int directed; //是否有向
}MGraph;
/* 1.初始化邻接矩阵
* graph :哪个图
* names[] : 顶点名称
* num :顶点数量
* directed :是否有向
* edgeValue :用来初始化边 0 ? ∞
*/
void initMGraph(MGraph* graph, char* names[], int num, int directed, int edgeValue);
/*
* 2.向邻接表添加边
* graph: 向哪个图添加边
* x : 起始顶点的编号
* y : 终点顶点的编号
* w : 边的权值
*/
void addMGraphEdge(MGraph* graph, int x, int y, int w);
c
void initMGraph(MGraph* graph, char* names[], int num, int directed, int edgeValue)
{
//初始化 是否有向 顶点数量 边的数量
graph->directed = directed;
graph->nodeNum = num;
graph->edgeNum = 0;
//初始化 顶点集合 边的集合
memset(graph->vex, 0, sizeof(graph->vex));
memset(graph->edges, 0, sizeof(MatrixEdge) * MaxNodeNum * MaxNodeNum);
//初始化顶点编号 显示行为 边
for (int i = 0;i < num;i++)
{
graph->vex[i].no = i;
graph->vex[i].show = names[i];
for (int j = 0;j < num;j++)
{
graph->edges[i][j] = edgeValue;
}
}
}
void addMGraphEdge(MGraph* graph, int x, int y, int w)
{
if (x<0 || x>graph->nodeNum)
{
return;
}
if (y<0 || y>graph->nodeNum)
{
return;
}
if (!isEdge(graph->edges[x][y]))
{ //没边加边
graph->edges[x][y] = w;
if (graph->directed == 0)
{ //无向图
graph->edges[y][x] = w;
}
graph->edgeNum++;
}
}
5.1 头文件部分
c
/*
* 利用邻接矩阵 生成一个边集数组 利用边集数组实现Kruskal算法
* 算法思路:
* a.将所有的边按权值的大小进行升序排序 从小到大依此提取边的情况 满足条件
* b.加入这条边,和之前已经选中的边不能组成环路 如果不能 这条边就归入生成树的结果部分
* c.如果能,舍去,再去找下一条,直到筛选出n-1条边
*/
//边集数组的结构
typedef struct
{
int begin; //边的起点(顶点1)
int end; //边的终点(顶点2)
int weight; //边的权值
}EdgeSet;
//1. 从邻接矩阵中初始化边集数组
void initEdgeSet(const MGraph* graph, EdgeSet* edges);
//2. 排序边集数组 (选择排序)
void sortEdgeSet(EdgeSet* edges, int num);
//3. Kruskal算法
int KruskalMGraph(const EdgeSet* edges, int num, int node_num, EdgeSet* result);
5.2 函数实现
5.2.1 从邻接矩阵中初始化边集数组
c
void initEdgeSet(const MGraph* 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)
{//边权>0为有效边
edges[k].begin = i;
edges[k].end = j;
edges[k].weight = graph->edges[i][j];
k++;
}
}
}
}
5.2.2 排序边集数组 (选择排序)
c
/*因为是c语言 所以我们自己手写个排序*/
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));
}
}
}
}
5.2.3 Kruskal算法
c
/*并查集 用来判断是不是同一个根*/
static int getRoot(const int *Uset,int a)
{
while (Uset[a] != a)
{
a = Uset[a];
}
// 当Uset[a]==a时,a就是根节点,返回
return a;
}
//Kruskal算法
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; //a的父节点加到b b的父节点加到a也可以
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;
}
5.3 main 函数
c
void setupMGraph(MGraph *graph,int edageValue)
{ /* 0 1 2 3 4 5 6 */
char* names[] = { "A","B","C","D","E","F","G" };
initMGraph(graph, names, sizeof(names) / sizeof(names[0]), 0, edageValue);
//加边
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 test04()
{
MGraph graph; //邻接矩阵
EdgeSet* edges; //边
EdgeSet* result; //接受Krusal的结果
//初始化邻接矩阵
setupMGraph(&graph,0);
//申请边
edges = malloc(sizeof(EdgeSet) * graph.edgeNum);
if (edges == NULL)
{
return;
}
//初始化边集数组
initEdgeSet(&graph, edges);
//按权值 排序边
sortEdgeSet(edges, graph.edgeNum);
//排序后的边
/*showEdges(edges, graph.edgeNum)
printf("\n");*/
//最小生成树的边 是节点的个数减1
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", sum);
free(result);
free(edges);
}
int main()
{
test04();
return 0;
}
5.4 输出结果
c
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
- 测试案例可以参考这张图

嘻嘻嘻嘻 Kruskal 部分到此结束😆😆
Prim 静候更新 👻👻👻😸😸😸
(有错误欢迎指出) (疑问也是)❤️❤️😍😍💖💖
持续更新中...