数据结构(19)图的最小生成树算法

  • 前言
  • 普里姆(Prim)算法
  • 克鲁斯卡尔(Kruskal)算法
  • 代码
    • GraphMtx.h
    • GraphMtx.c
    • Main.c

前言

在有n个顶点的图中,要连接所有顶点,只需要n-1条线路。假设每假设一条线路都需要付出一定代价,并且每条线路的代价不一定相同,那么就引出一个新的问题:如何设置线路,使得所有线路的总代价最小呢?

一个连通图的生成树是一个极小的连通子图,它含有图中的全部顶点,但是只有足以构成一棵树的n-1条边;当某棵生成树所拥有的n-1条边的代价总和为最小时,称其为最小生成树。

如何找到这样一棵最小生成树呢?主要有两种算法,即普里姆(Prim)算法和克鲁斯卡尔(Kruskal)算法。

普里姆(Prim)算法

普里姆算法的思想是:从已纳入生成树的顶点集合出发,找到下一个可以到达并且花费代价为最小的顶点,若它不在生成树中,则将其纳入生成树。

显然,算法要求先拥有一个生成树的顶点集合,这意味着在调用算法时,需要指定一个初始的顶点,先把它纳入集合中。

以上图为例,假设初始顶点为A,那么普里姆算法是如何寻找最小生成树的呢?

  1. 首先,将A顶点纳入顶点集合中,此时可以到达的顶点为B、C、D,其中花费代价最小的顶点为C(A-C),并且C不在顶点集合中,则将C纳入生成树

  2. A、C在顶点集合中,此时可以到达的顶点为B、D、E、F,其中花费代价最小的顶点为F(C-F),并且F不在顶点集合中,则将F纳入生成树

  3. A、C、F在顶点集合中,此时可以到达的顶点为B、D、E,其中花费代价最小的顶点为D(F-D),并且D不在顶点集合中,则将D纳入生成树

  1. A、C、D、F在顶点集合中,此时可以到达的顶点为B、E,其中花费代价最小的顶点为B(C-B),并且B不在顶点集合中,则将B纳入生成树

  2. A、B、C、D、F在顶点集合中,此时可以到达的顶点为E,其中花费代价最小的顶点为E(B-E),并且E不在顶点集合中,则将E纳入生成树

这样我们就获得了一个最小生成树,每个顶点之间边的权值总和最小。

那么,如何实现这个算法呢?

可以发现,我们只关心生成树的顶点集合到剩余顶点的最小花费。例如,在C顶点加入顶点集合后,顶点集合到B顶点有两条边,即A-B(6)与C-B(5),如果此时需要选择一条连接B顶点,必然会选择C-B;即使以后出现比C-B花费更少的边,也与A-C边无关。

也就是说,我们可以维护一个lowCost数组,它记录了顶点集合到每个顶点的最小花费,如果顶点已经在集合中,则花费为0;如果有未能到达的顶点,则花费为max;否则存放顶点集合中的某个顶点到它的花费(这条线路的花费是最小的)。

那么问题来了:当顶点集合有多个顶点时,我们怎么知道是哪个顶点到目标顶点的花费呢?因此还需要维护一个mst数组,来记录lowCost中的花费,是从哪个顶点出发产生的。

这样,当有新的顶点纳入生成树时,首先将lowCost数组中该顶点的花费置为0,然后从该顶点出发,重新计算到达每个顶点的花费,如果小于已存在的花费,则修改lowCost数组和mst数组的数据。

然后,再遍历新的lowCost数组,找到下一个花费最小的顶点,将其纳入生成树。

实现普里姆算法的整体思路是:

  • 将两个辅助数组创建好
  • 获取到初始顶点的位置,将其纳入生成树中(这个过程相当于初始化两个辅助数组
  • 开始遍历lowCost数组,每次找到最小花费可到达的顶点,将该顶点纳入生成树(重新计算顶点集合到每个顶点的最小花费)
  • 有n个顶点,则需要找到n-1条边,即循环n-1次
//获取两个顶点的距离
int GetWeight(GraphMtx *g,int v1,int v2){if (v1 == -1 || v2 == -1) {return DEFAULT_MAX_COST;}return g->Edge[v1][v2];
}
//最小生成树-普里姆算法
void MinSpanTree_Prim(GraphMtx *g,T vertex){//辅助数组->记录最小的花费int *lowCost = (int*)malloc(sizeof(int)*g->NumVertices);assert(lowCost != NULL);//辅助数组->记录起始顶点int *mst = (int *)malloc(sizeof(int)*g->NumVertices);assert(mst != NULL);//获得起始位置int k = GetVertexPos(g, vertex);//将初始顶点纳入生成树->初始化辅助数组中的数据for (int i = 0; i < g->NumVertices; i ++) {if (i != k) {lowCost[i] = GetWeight(g, k, i);mst[i] = k;}else{lowCost[i] = 0;}}int min,minIndex;int begin,end;int cost;for (int i = 0; i < g->NumVertices-1; i ++) {min = DEFAULT_MAX_COST;minIndex = -1;//找到当前最小花费可到达的顶点for (int j = 0; j < g->NumVertices; j ++) {if (lowCost[j] != 0 && lowCost[j] < min) {//如果它没有存在于顶点集合中->找到了min = lowCost[j];minIndex = j;}}//找到到达该顶点的起始点begin = mst[minIndex];end = minIndex;//输出信息printf("%c->%c:%d\n",g->VerticesList[begin],g->VerticesList[end],min);//将该顶点纳入生成树lowCost[minIndex] = 0;//重新计算到达每个顶点的最小花费for (int j = 0; j < g->NumVertices; j ++) {cost = GetWeight(g, minIndex, j);if (cost < lowCost[j]) {//该顶点的花费比已存的花费少->更新数据lowCost[j] = cost;mst[j] = minIndex;}}}
}

克鲁斯卡尔(Kruskal)算法

如果说,普里姆算法是从顶点出发,去寻找顶点,那么克鲁斯卡尔算法出发则是从边出发来寻找顶点。

其基本思想是:每次寻找当前花费最小的一条边,若该边的两条顶点不在同一个连通子图上,则将其加入到生成树中。

如图所示

  1. 首先找到当前花费最小的边A-C,A、C顶点不在同一个连通子图上,将其加入生成树中

  2. 找到当前花费最小的边D-F,D、F顶点不在同一个连通子图上,将其加入生成树中

  3. 找到当前花费最小的边B-E,B、E顶点不在同一个连通子图上,将其加入生成树中

  1. 找到当前花费最小的边C-F,C、F顶点不在同一个连通子图上,将其加入生成树中

  2. 找到当前花费最小的边,此时有三条边代价相等,即B-C、A-D、C-D,选择任意一条边即可。

    但是可以发现,此时A、C、D顶点在同一个连通子图上,显然不能连接。因此最终选择B-C边,加入生成树中

这样,同样可以得到一棵最小生成树。

那么,如何判断两个顶点是否在同一个连通子图呢?

我们可以设置一个father数组,用于记录每一个顶点的父结点,假如两个顶点拥有相同的父结点,则说明是在同一个连通子图上。假如顶点的父结点是它本身,说明该顶点就是一个连通子图的根;若不是它本身,则说明它之上还有父结点,则继续向上查找。

  • 当father数组初始化时,每个顶点的父结点都是它本身,说明此时每个顶点都是相互独立的。

  • 将A-C边纳入生成树中,则令C顶点的父结点为A(反之亦可),此时A、C顶点连成一个连通子图,之后的D-F、B-E边同理

  • 将C-F纳入生成树中,先判断C的父结点(A)与F的父结点(F)是否是同一个,不是则可以纳入;令F的父结点的父结点等于C的父结点(反之亦可),这样A-C-F-D连成了一个连通子图

    注:假设,F的父结点为D,而D的父结点为它本身,则C-F连接时,是令F的父结点(D)的父结点等于C的父结点(A)。相反地,也可以让C的父结点(A)的父结点等于F的父结点(D)

    注注:假设此时,想插入A-D边,则查找A的父结点(A)与D的父结点。虽然father数组中D的父结点为F,但是F的父结点不是它本身,说明其上还有父结点,则继续寻找到F的父结点(A),则A与D拥有同一个父结点,说明是在同一个连通子图中,无法插入

  • 将B-C纳入生成树中,先判断C的父结点(A)与B的父结点(B)是否是同一个,不是则可以纳入;令B的父结点的父结点等于C的父结点(反之亦可),这样A-B-C-D-E-F连成了一个连通子图

可见,克鲁斯卡尔算法有两个主要部分:第一个即判断两个结点的父结点是否是同一个,第二是使一个顶点的父结点的父结点等于另一个顶点的父结点(也就是将两个连通子图连在一起)

int Is_same(int *father,int i,int j){//找到i的父结点while (father[i] != i) {i = father[i];}//找到j的父结点while (father[j] != j) {j = father[j];}//判断是否一致return i == j ? 1 : 0;
}void Mark_same(int *father,int i,int j){//找到i的父结点while (father[i] != i) {i = father[i];}//找到j的父结点while (father[j] != j) {j = father[j];}//使j的父结点的父结点等于i的父结点father[j] = i;
}

克鲁斯卡尔算法是从边出发,而我们选用邻接矩阵作为图的存储方式,并没有存储边的信息。因此在这里我们需要设计一个边的结构,来保存边的两个顶点,和权值。

//边结构
typedef struct Edge{//边的起点int x;//边的终点int y;//边的权值int cost;
}Edge;

因此,在真正实施克鲁斯卡尔算法时,还需要先将所有边统计出来。同时,由于每次都去寻找代价最小的边,因此可以先根据代价对边进行排序,这样更方便些。


//根据权值对边排序
int cmp(const void *a,const void *b){return ((*(Edge *)a).cost - (*(Edge *)b).cost);
}
//最小生成树-克鲁斯卡尔算法
void MinSpanTree_Kruskal(GraphMtx *g){int n = g->NumVertices;Edge *edge = (Edge *)malloc(sizeof(Edge) * (n*(n-1)/2));assert(edge != NULL);//找到所有边int k = 0;for (int i = 0; i < n; i ++) {for (int j = i; j < n; j ++) {if (g->Edge[i][j] != 0 && g->Edge[i][j] != DEFAULT_MAX_COST) {edge[k].x = i;edge[k].y = j;edge[k].cost = g->Edge[i][j];k ++;}}}//将所有边进行排序qsort(edge, k, sizeof(Edge),cmp);//初始化父结点数组int *father = (int *)malloc(sizeof(int) * n);assert(father != NULL);for (int i = 0; i < n; i ++) {father[i] = i;}int v1,v2;for (int i = 0; i < n; i ++) {if (!Is_same(father,edge[i].x,edge[i].y)) {//两个顶点不在同一个连通子图上->使其连接v1 = edge[i].x;v2 = edge[i].y;printf("%c->%c : %d\n",g->VerticesList[v1],g->VerticesList[v2],edge[i].cost);Mark_same(father,edge[i].x,edge[i].y);}}}

代码

GraphMtx.h

#ifndef GraphMtx_h
#define GraphMtx_h#include <stdio.h>
#include <stdlib.h>
#include <assert.h>//默认的顶点个数
#define DEFAULT_VERTEX_SIZE 10
//初始化的距离
#define DEFAULT_MAX_COST 0x7FFFFFF
//顶点的数据类型
#define T char//边结构
typedef struct Edge{//边的起点int x;//边的终点int y;//边的权值int cost;
}Edge;typedef struct GraphMtx{//最大的顶点数int MaxVertices;//现有的顶点数int NumVertices;//现有的边数int NumEdges;//顶点列表T *VerticesList;//边->矩阵int **Edge;
}GraphMtx;//初始化
void InitGraph(GraphMtx *g);//展示图
void ShowGraph(GraphMtx *g);//获取顶点位置
int GetVertexPos(GraphMtx *g,T v);//插入顶点
void InsertVertex(GraphMtx *g,T v);
//插入边-无向
void InsertEdge(GraphMtx *g,T v1,T v2,int cost);//摧毁图
void DestoryGraph(GraphMtx *g);//求v1到v2边的权值
int GetWeight(GraphMtx *g,int v1,int v2);
//最小生成树-普利姆算法
void MinSpanTree_Prim(GraphMtx *g,T vertex);
//最小生成树-克鲁斯卡尔算法
void MinSpanTree_Kruskal(GraphMtx *g);int cmp(const void *a,const void *b);
void Mark_same(int *father,int i,int j);
#endif /* GraphMtx_h */

GraphMtx.c

#include "GraphMtx.h"//初始化
void InitGraph(GraphMtx *g){//数据初始化g->MaxVertices = DEFAULT_VERTEX_SIZE;g->NumVertices = g->NumEdges = 0;//开辟储存顶点的空间g->VerticesList = (T*)malloc(sizeof(T) * g->MaxVertices);assert(g->VerticesList != NULL);//开辟储存边的空间g->Edge = (int **)malloc(sizeof(int*) * g->MaxVertices);assert(g->Edge != NULL);for (int i = 0; i < g->MaxVertices; i ++) {g->Edge[i] = (int *)malloc(sizeof(int) * g->MaxVertices);assert(g->Edge[i] != NULL);}//初始化边for (int i = 0; i < g->MaxVertices; i ++) {for (int j = 0; j < g->MaxVertices; j ++) {if (i == j) {g->Edge[i][j] = 0;}else{g->Edge[i][j] = DEFAULT_MAX_COST;}}}
}//展示图
void ShowGraph(GraphMtx *g){//打印第一排的顶点printf("  ");for (int i = 0; i < g->NumVertices; i ++) {printf("%c ",g->VerticesList[i]);}printf("\n");//打印边->矩阵for (int i = 0; i < g->NumVertices; i ++) {printf("%c ",g->VerticesList[i]);for (int j = 0; j < g->NumVertices; j ++) {if (g->Edge[i][j] == DEFAULT_MAX_COST) {printf("%s ","@");}else{printf("%d ",g->Edge[i][j]);}}printf("\n");}printf("\n");
}//获取顶点位置
int GetVertexPos(GraphMtx *g,T v){for (int i = 0; i < g->NumVertices; i ++) {if (g->VerticesList[i] == v) {return i;}}return -1;
}//插入顶点
void InsertVertex(GraphMtx *g,T v){if (g->NumVertices == g->MaxVertices) {printf("顶点已满,无法插入\n");return;}g->VerticesList[g->NumVertices ++] = v;
}
//插入边
void InsertEdge(GraphMtx *g,T v1,T v2,int cost){//获取v1的位置int p1 = GetVertexPos(g, v1);//获取v2的位置int p2 = GetVertexPos(g, v2);if (p1 == -1 || p2 == -1) {printf("有顶点不存在\n");return;}g->Edge[p1][p2] = g->Edge[p2][p1] = cost;g->NumEdges ++;}//摧毁图
void DestoryGraph(GraphMtx *g){//释放顶点空间free(g->VerticesList);g->VerticesList = NULL;for (int i = 0; i < g->NumVertices; i ++) {free(g->Edge[i]);}g->Edge = NULL;g->MaxVertices = g->NumEdges = g->NumVertices = 0;
}
//求v1与v2边的权值
int GetWeight(GraphMtx *g,int v1,int v2){if (v1 == -1 || v2 == -1) {return DEFAULT_MAX_COST;}return g->Edge[v1][v2];
}
//最小生成树-普里姆算法
void MinSpanTree_Prim(GraphMtx *g,T vertex){//辅助数组->记录最小的花费int *lowCost = (int*)malloc(sizeof(int)*g->NumVertices);assert(lowCost != NULL);//辅助数组->记录起始顶点int *mst = (int *)malloc(sizeof(int)*g->NumVertices);assert(mst != NULL);//获得起始位置int k = GetVertexPos(g, vertex);for (int i = 0; i < g->NumVertices; i ++) {if (i != k) {lowCost[i] = GetWeight(g, k, i);mst[i] = k;}else{lowCost[i] = 0;}}int min,minIndex;int begin,end;int cost;for (int i = 0; i < g->NumVertices-1; i ++) {min = DEFAULT_MAX_COST;minIndex = -1;for (int j = 0; j < g->NumVertices; j ++) {if (lowCost[j] != 0 && lowCost[j] < min) {min = lowCost[j];minIndex = j;}}begin = mst[minIndex];end = minIndex;printf("%c->%c:%d\n",g->VerticesList[begin],g->VerticesList[end],min);lowCost[minIndex] = 0;for (int j = 0; j < g->NumVertices; j ++) {cost = GetWeight(g, minIndex, j);if (cost < lowCost[j]) {lowCost[j] = cost;mst[j] = minIndex;}}}
}//最小生成树-克鲁斯卡尔算法
void MinSpanTree_Kruskal(GraphMtx *g){int n = g->NumVertices;Edge *edge = (Edge *)malloc(sizeof(Edge) * (n*(n-1)/2));assert(edge != NULL);//找到所有边int k = 0;for (int i = 0; i < n; i ++) {for (int j = i; j < n; j ++) {if (g->Edge[i][j] != 0 && g->Edge[i][j] != DEFAULT_MAX_COST) {edge[k].x = i;edge[k].y = j;edge[k].cost = g->Edge[i][j];k ++;}}}//将所有边进行排序qsort(edge, k, sizeof(Edge),cmp);//初始化父结点数组int *father = (int *)malloc(sizeof(int) * n);assert(father != NULL);for (int i = 0; i < n; i ++) {father[i] = i;}int v1,v2;for (int i = 0; i < n; i ++) {if (!Is_same(father,edge[i].x,edge[i].y)) {//两个顶点不在同一个连通子图上->使其连接v1 = edge[i].x;v2 = edge[i].y;printf("%c->%c:%d\n",g->VerticesList[v1],g->VerticesList[v2],edge[i].cost);Mark_same(father,edge[i].x,edge[i].y);}}}int cmp(const void *a,const void *b){return ((*(Edge *)a).cost - (*(Edge *)b).cost);
}int Is_same(int *father,int i,int j){//找到i的父结点while (father[i] != i) {i = father[i];}//找到j的父结点while (father[j] != j) {j = father[j];}//判断是否一致return i == j ? 1 : 0;
}void Mark_same(int *father,int i,int j){//找到i的父结点while (father[i] != i) {i = father[i];}//找到j的父结点while (father[j] != j) {j = father[j];}//使j的父结点的父结点等于i的父结点father[j] = i;
}

Main.c


#include "GraphMtx.h"int main(int argc, const char * argv[]) {GraphMtx gm;InitGraph(&gm);InsertVertex(&gm, 'A');InsertVertex(&gm, 'B');InsertVertex(&gm, 'C');InsertVertex(&gm, 'D');InsertVertex(&gm, 'E');InsertVertex(&gm, 'F');InsertEdge(&gm,'A','B',6);InsertEdge(&gm,'A','C',1);InsertEdge(&gm,'A','D',5);InsertEdge(&gm,'B','C',5);InsertEdge(&gm,'B','E',3);InsertEdge(&gm,'C','D',5);InsertEdge(&gm,'C','E',6);InsertEdge(&gm,'C','F',4);InsertEdge(&gm,'D','F',2);InsertEdge(&gm,'E','F',6);ShowGraph(&gm);printf("prim:\n");MinSpanTree_Prim(&gm, 'A');printf("\n");printf("kruskal:\n");MinSpanTree_Kruskal(&gm);DestoryGraph(&gm);return 0;
}

数据结构(19)图的最小生成树算法相关推荐

  1. 【数据结构】——图的最小生成树算法(普里姆+克鲁斯卡尔)

    这里的图指的是带权无向图,也就是无向网. 关于最小生成树 图的最小生成树要解决的问题:用最小的代价连通图中的所有顶点. 下面两种算法都是运用贪心思想,利用MST(Minimum Spanning Tr ...

  2. 【数据结构】图的最小生成树算法

    假设你是电信的实施工程师,需要为一个镇的九个村庄架设通信网络做设计,村 庄位置大致如图,其中 Vo-V8是村庄,之间连线的数字表示村与村间的可通达 的直线距离,比如Vo至V1就是10公里(个别如Vo与 ...

  3. 八十五、Python | Leetcode数据结构之图和动态规划算法系列

    @Author:Runsen @Date:2020/7/7 人生最重要的不是所站的位置,而是内心所朝的方向.只要我在每篇博文中写得自己体会,修炼身心:在每天的不断重复学习中,耐住寂寞,练就真功,不畏艰 ...

  4. (数据结构)图的最小生成树 普里姆算法(Prim)

    假设要在n个城市之间建立通信联络网,每两个城市之间建立线路都需要花费不同大小的经费,则连通n个城市只需要n-1个条线路,最小生成树解决的问题就是:如何在最节省经费的前提下建立这个通信网 也可以理解为: ...

  5. 数据结构值图的最小生成树

    最小生成树(最小连通网) 假设在n个城市之间建立通信联络网,则连通n个城市只需要n-1条线路.这时自然会考虑这样一个问题,如何在最节省经费的前提下建立这个通信网. 在每两个城市之间都可以设置一条线路, ...

  6. 沃舍尔算法_[数据结构拾遗]图的最短路径算法

    前言 本专题旨在快速了解常见的数据结构和算法. 在需要使用到相应算法时,能够帮助你回忆出常用的实现方案并且知晓其优缺点和适用环境.并不涉及十分具体的实现细节描述. 图的最短路径算法 最短路径问题是图论 ...

  7. 图的最小生成树算法(图解+代码)| 学不会来看我系列

    文章目录 最小生成树 Prim算法 1.介绍 2.图解步骤 3.算法分析 算法问题 解决方案 4.代码实现 Kruskal算法 1.介绍 2.图解 3.算法分析 算法问题 解决方案 4.代码实现 最小 ...

  8. 图的最小生成树算法实现(Prim + Kruskal)

    1.采用书上第 161 页定义的图的邻接矩阵存储表示,编程实现构造最小生成树的 Prim 算法. 2.采用书上第 161 页定义的图的邻接矩阵存储表示,编程实现构造最小生成树的 Kruskal 算法. ...

  9. 【数据结构】图—克鲁斯卡尔算法(原理及C程序详解)

    普利姆(Prim)算法是以某个顶点为起点,逐步寻找各个顶点上权值最小的边来构建生成树. 文章指引:图-普里姆(Prim)算法(原理和C程序解释) 而克鲁斯卡尔(Kruskal)算法是以边为目标,直接寻 ...

最新文章

  1. 栈 -- 顺序栈、链式栈的实现 及其应用(函数栈,表达式求值,括号匹配)
  2. STM32的SPI问题。
  3. Item 16: 让const成员函数做到线程安全
  4. 最简单的基于librtmp的示例:发布H.264(H.264通过RTMP发布)
  5. LeetCode 1564. 把箱子放进仓库里 I(排序)
  6. python自动化安装软件_python自动化安装源码软件包
  7. 【剑指offer】面试题14- I:剪绳子(Java)
  8. ASP.NET组件设计Step by Step(8)
  9. 【语音识别】基于matlab带动量项的BP神经网络语音识别【含Matlab源码 430期】
  10. java -jar bat_java jar包和bat批处理命令
  11. java中int型的最大值_java int 的最大值 Integer.MAX_VALUE
  12. 国务院《新能源汽车产业发展规划(2021—2035年)》
  13. Google浏览器离线安装包下载
  14. [xctf] 江苏工匠杯easyphp
  15. jqwidgets 国际化- 中文 jqxGrid 中文语言包 gridlocalization
  16. RTMP流媒体直播资料
  17. 美国时间格式化成通用时间
  18. 面向2018年的设计趋势
  19. H-1B身份六年后的延期问题
  20. echarts为什么用getElementsByClassName()方法显示不了图表

热门文章

  1. 实验08 软件设计模式及应用
  2. 深度优先搜索中的树边、后向边,前向边和交叉边
  3. 为你揭露2018微信公开课pro的12个重点
  4. 操作系统-PV操作-独木桥问题
  5. 如何从电压范围75V-3500V中选购合适的GDT-陶瓷气体放电管-优恩
  6. [操作系统] 驻留集和工作集的辨析
  7. tomcat tomcat配置 项目部署tomcat三种方式
  8. 李彦宏:去年“吹的牛”我兑现了!百度无人车今天正式量产!
  9. 使用C4D灵动诠释宇舶表限量版陀飞轮全蓝宝石腕表的冰肌玉骨
  10. 解析learn the python3 the hard way里的ex43