一.前言
什么是最小生成树?假设我们有一个连通
的网(弧或边带权的图),在这个网里,我们需要构建一个(强)连通分量,且该连通分量的权的和最小。举个例子,就像给你一个镇的地图,在已知的所有路上,新修一条造价最小的村村通。
二.普里姆算法
1.原理
普里姆算法的思想就是,有顶点集U,和已确定的顶点集V。然后:
- 从U里任意取一个顶点,然后加入到V中。
- 从(U-V)的集合里取一个元素,找一条弧(边)从该元素到V里顶点权最小的,则是我们的弧。
- 吧该元素加入V中。
- 重复2,3步,直到V=U。
这样,且除了我们第一个顶点没有权值,其余的点都有权值,刚好是n个顶点,n-1条边(弧),这就是一个连通分量而且是权和最小的连通分量。
因为普里姆算法和顶点有关系,和边没有关系,所以最适合稠密图。
2.流程
现在放个图作为演示:
- 首先随便选个结点,由于是线性存储的网,所以一般就是A
- 然后重复在蓝色的点中找一个顶点,且与黄色的顶点中有最短的边:
然后最终效果:
三.克鲁斯卡尔算法
1.原理
刚才我们的普里姆算法的思路是,通过顶点去遍历边,现在我们的克鲁斯卡尔算法是通过边去遍历边。其流程如下:
- 将所有边按权的升序排列。
- 从最小的边开始遍历,将边的两个顶点标记成相同的值。
- 若有一个有值,则给没有值得那个标记为有值的那个顶点的值。
- 如果两个 都有值,若值相同则舍去(会变成一个环),若不相同,则都标记为同一个值。
- 直到标记完所有的顶点。
克鲁斯卡尔由于是用边来做计算,而没使用顶点,所以最优适用于稀疏图
2.流程
假设我们对边和顶点排好了序:
然后第一条边:
第二条边:
第三条边:
第四条边:
第五条边,不行,第六条边行,第七条边不行:
此时已经完成了,那就结束惹:
3.并查集
可能我们在使用这种方法的时候,可能不方便写代码,那么采用并查集的方法是比较简单的。
首先,为了区别两个结点是否相同,我们可以将结点设置为树形结构,将结点指向一个父结点,只要这一段程序层层上去,父结点相同即相同。比如:
那么下一步,并查集会变成:
再下一步:
当然,并查集的操作,我们也可以用数组模拟啦。
四.代码
因为上篇文章用的邻接表,所以为了看看图的不同储存类型的代码,这篇文章用领接矩阵来实现,而且普里姆用领接矩阵更好实现。
1.普里姆
普里姆算法里的MinRight[]
数组需要多留意,它是一种叠加态的数组,叠加的是当前已经遍历的顶点,到其他顶点的最小权。
代码
#include <stdio.h>
#include <stdlib.h>
#define OK 1
#define ERROR 0
#define MaxLen 100
typedef struct Graph{
int *map;
char *vertexName;
int NodeNum;
}Graph; // 定义图
int * GetValueAdd(Graph *gmap, int row, int col);
Graph* GraphInit(void);
int GraphShow(Graph *gmap);
int PrimMST(Graph *gmap);
int main()
{
Graph *gmap = GraphInit();
GraphShow(gmap);
PrimMST(gmap);
return 0;
}
int * GetValueAdd(Graph *gmap, int row, int col)
{
if (gmap == NULL)
{
exit(ERROR);
} // 返回指定下标的地址
return (gmap->map + (gmap->NodeNum) * row + col);
}
Graph* GraphInit(void)
{
Graph *gmap = (Graph*)malloc(sizeof(Graph));
int vernum, edgenum, end, start, right;
char vname;
int i, j;
int *temp;
// 创建图的储存结构
printf("\nPlease input the number of the vertex and edge:");
scanf("%d %d", &vernum, &edgenum);
gmap->NodeNum = vernum;
gmap->map = (int*)malloc(sizeof(int) * vernum * vernum);
gmap->vertexName = (char*)malloc(sizeof(char) * vernum);
// 初始化图为-1
for (i = 0; i < gmap->NodeNum; ++i)
{
for (j = 0; j < gmap->NodeNum; j++)
{
temp = GetValueAdd(gmap, i, j);
*temp = -1;
}
}
// 存顶点名
rewind(stdin);
printf("Please input nodes's name:\n");
for (i = 0; i < gmap->NodeNum; ++i)
{
scanf("%c", &vname);
*(gmap->vertexName + i) = vname;
}
// 存图的权
printf("Please input vetrex of the edges and right:\n");
for (i = 0; i < edgenum; ++i)
{
scanf("%d %d %d", &end, &start, &right);
temp = GetValueAdd(gmap, end, start);
*temp = right;
temp = GetValueAdd(gmap, start, end);
*temp = right;
}
return gmap;
}
int GraphShow(Graph *gmap)
{
int i ,j, *temp;
// 先打印 一行顶点名
printf("Show the Map:\n ");
for (i = 0; i < gmap->NodeNum; ++i)
{
printf("%3c", *(gmap->vertexName+i));
}
// 打印邻接矩阵
printf("\n");
for (i = 0; i < gmap->NodeNum; ++i)
{
printf("%c ", *(gmap->vertexName+i));
for (j = 0; j < gmap->NodeNum; ++j)
{
temp = GetValueAdd(gmap, i, j);
printf("%3d", *temp);
}
printf("\n");
}
return OK;
}
int PrimMST(Graph *gmap)
{
int *Vertex = (int*)malloc(sizeof(int) * (gmap->NodeNum));
int *MinRight = (int*)malloc(sizeof(int) * gmap->NodeNum);
int i, j, min, cont;
// MinRight的定义是:当前已遍历集到其他未遍历的顶点的最小权的叠加态
// 可能不好理解,自己画图模拟下流程会更好理解。
printf("MST:\n");
// 先初始化,把顶点集填充为首顶点,且权为首顶点的集。
for (i = 0; i < gmap->NodeNum; ++i)
{
Vertex[i] = 0;
// 所以当前已遍历集合的权为首顶点的权
MinRight[i] = *GetValueAdd(gmap, 0, i);
}
for (cont = 1; cont < gmap->NodeNum; ++cont)
{
min = 0xffff;
i = 1;
j = 0;
// 在其他叠加权里找最小的下一个顶点
while(i < gmap->NodeNum)
{
if (MinRight[i] > 0 && MinRight[i] < min)
{
min = MinRight[i];
j = i;
}
++i;
}
// 输出最小弧的两个顶点
printf("%c -> %c\n", *(gmap->vertexName + Vertex[j]), *(gmap->vertexName + j));
MinRight[j] = 0;
// 这里是更新叠加权
// 此时加入了新找到的顶点,叠加权可能不是最小的。
// 要将新加入的顶点到其他顶点的权与叠加权对比
// 如果该顶点到某个顶点的权比叠加权小,就更新叠加权
// 确保MinRight是最小的已遍历顶点到未遍历顶点的叠加权集。
for(i = 1; i < gmap->NodeNum; ++i)
{
if (MinRight[i] > 0 && *(GetValueAdd(gmap, j, i)) < MinRight[i])
{
MinRight[i] = *(GetValueAdd(gmap, j, i));
Vertex[i] = j;
}
}
}
return 0;
}
2.克鲁斯卡尔
样例输入与输出:
Please input the number of Vextex and Edge:
6 10
Please input Vexters' name:
ABCDEF
Please input edge tail head right:
0 1 6
0 3 5
0 2 1
1 2 5
2 3 5
1 4 3
2 4 6
2 5 4
3 5 2
4 5 6
Graph Edges:
A <--1--> C
D <--2--> F
B <--3--> E
C <--4--> F
C <--5--> D
B <--5--> C
A <--5--> D
A <--6--> B
C <--6--> E
E <--6--> F
A <----> C
D <----> F
B <----> E
C <----> F
B <----> C
代码:
#include <stdio.h>
#include <stdlib.h>
#define OK 1
#define ERROR 0
#define MaxLen 100
typedef struct Edge{
int tail;
int head;
int right;
}Edge; // 定义一段边
typedef struct Graph{
Edge *ebase;
int Edgenum;
char *vexter;
int vexnum;
}Graph; // 定义一个边
Graph* GraphInit(void);
int GraphShow(Graph *gmap);
int GEdgeSort(Graph *gmap);
int disjointSetFind(int *disjoint, int a, int b);
int KruskalMST(Graph *gmap);
int main()
{
Graph *gmap = GraphInit();
GEdgeSort(gmap);
GraphShow(gmap);
KruskalMST(gmap);
return 0;
}
Graph* GraphInit(void)
{
Graph *gmap = (Graph*)malloc(sizeof(Graph));
int vexnum, edgenum;
char temp;
int i, head, tail, right;
printf("Please input the number of Vextex and Edge:\n");
scanf("%d%d", &vexnum, &edgenum);
// 创建一个图
gmap->ebase = (Edge*)malloc(sizeof(Edge) * edgenum);
gmap->vexter = (char*)malloc(sizeof(char) * vexnum);
gmap->Edgenum = edgenum;
gmap->vexnum = vexnum;
// 创建顶点名
printf("Please input Vexters' name:\n");
rewind(stdin);
for (i = 0; i < vexnum; ++i)
{
scanf("%c", &temp);
gmap->vexter[i] = temp;
}
// 创建边
printf("Please input edge tail head right:\n");
rewind(stdin);
for (i = 0; i < edgenum; ++i)
{
scanf("%d %d %d", &tail, &head, &right);
(gmap->ebase+i)->tail = tail;
(gmap->ebase+i)->head = head;
(gmap->ebase+i)->right = right;
}
// 返回图
return gmap;
}
int GraphShow(Graph *gmap)
{
int i, tail, head, right;
// 输出边
printf("Graph Edges:\n");
for (i = 0; i < gmap->Edgenum; ++i)
{
tail = (gmap->ebase + i)->tail;
head = (gmap->ebase + i)->head;
right = (gmap->ebase + i)->right;
printf("%c <--%d--> %c\n", *(gmap->vexter+tail), right, *(gmap->vexter+head));
}
printf("\n");
return OK;
}
int GEdgeSort(Graph *gmap)
{
Edge *min, temp;
int i, j;
// 这是一个选择排序,每次把未排序的最小的值放到前面。
for (i = 0; i < gmap->Edgenum; ++i)
{
min = gmap->ebase + i;
for (j = i; j < gmap->Edgenum; ++j)
{
if ((gmap->ebase + j)->right < min->right)
{
min = gmap->ebase + j;
}
}
temp = *(gmap->ebase + i);
*(gmap->ebase + i) = *min;
*min = temp;
}
return OK;
}
// 这个函数是用于查找两个数在并查集里的关系,并带上路径优化
int disjointSetFind(int *disjoint, int a, int b)
{
// 用于计数,记录a和b循环了几次到达了根结点
int conta = 0, contb = 0;
// 记录ab的根结点的下标,作为标记
int afather = a, bfather = b;
// 假设a的值是大于0的,说明不是根结点
while(a > 0)
{
// 那么把此时的a当做根结点的父结点
afather = a;
// 然后迭代,查看该结点指向的结点
a = disjoint[a];
// 计次+1
++conta;
}
while(b > 0)
{
// b同理
bfather = b;
b = disjoint[b];
++contb;
}
// 如果两个根结点的父结点标记相同,则表示他们相同,返回0
if (afather == bfather)
{
return 0;
}
else
{
// 如果a结点的遍历次数比较少,则返回-b,用于让所有标记为a的变成b
if (conta < contb)
{
return -bfather;
}
else
{
// 否则遍历b结点的次数比较少,则返回a,用于让所有标记为b的变成a
return afather;
}
}
}
int KruskalMST(Graph *gmap)
{
// 创建一个并查集
int *disjointSet = (int*)malloc(sizeof(int) * gmap->vexnum);
int i, cont = 0, edgi = 0, temp;
int sta, tail, head;
// 初始化并查集,把每一个顶点都标记为-1
for (i = 0; i < 5; ++i)
{
disjointSet[i] = -1;
}
// 循环结束的条件, 边=顶点-1,
while (cont < gmap->vexnum - 1)
{
// 获取当前边的两个顶点
tail = (gmap->ebase + edgi)->tail;
head = (gmap->ebase + edgi)->head;
// 获取两个顶点的关系
sta = disjointSetFind(disjointSet, tail, head);
// 下次遍历就是下一条边
++edgi;
// 非零代表两者不属于一个集,可以结合
if (sta != 0)
{
// 先输出顶点信息
printf("%c <----> %c\n", *(gmap->vexter+tail), *(gmap->vexter+head));
// 若sta大于0,则head(b)结点的遍历次数较少
if (sta > 0)
{
// 把head有关的集的标记都变成 tail的标记
while(head > 0)
{
temp = head;
head = disjointSet[head];
disjointSet[temp] = sta;
}
}
else // 反之若sta小于0,则tail(a)结点的遍历次数较少
{
// 把tail有关的集的标记都变成 head的标记
while(tail > 0)
{
temp = tail;
tail = disjointSet[tail];
// 因为便于区分,所以让sta是小于0的,所以得用这个
disjointSet[temp] = -sta;
}
}
// 找到了一条边,数量+1
++cont;
}
}
}