文章目录
- 最小生成树
- 1.Prim算法
- 2. Kruskal
- 并查集
- 算法思路
最小生成树
1.Prim算法
假设你是一名铁路工人,需要为九个村庄建设铁路,村庄设计如下:
v
那么我们该如何找到最短路线尼。
首先我们先构造出上面图的邻接矩阵。
假设我们先从v0开始,v0旁边有两条边,权值分别是11和10,那么我们选10,10更小一点,所以v0和v1先连在一起,继续,看v0和v1的边,发现是11,16,12,18,显然11最小,所以此时v0和v5构成了树的第二条边,同样道理,只要我们不断这样下去,每次看已连接的点的边,那么最终会得到最小生成树。
所以我们定义一个lowost数组,来判断其是否已经连接在树中,再用一个adjvex数组,初始化全为0,来记录哪两个点之间是相连的,即记录下标。
所以我们刚开始得把v0的对应两个数组中的值变成0,表示他已经在树中,然后loecost数组的妙处就是和他有关的别的权值才有,否则都是无穷,所以很好判断,而这两个数组是相铺相成的,lowcost数组为adjvex数组值的变化提供了条件。
而这个算法的妙处就是一层一层的遍历,然后就相当于把每一个点连接的点又放进loecost数组中,然后记录他们的父亲节点的下标。
以下是核心代码
void prim(MGraph G){
int min,i,j,k;
int adjvex[MAXVEX];
int lowcost[MAXVEX];
lowcost[0] = 0;
adjvex[0] = 0;
for(i=1;i<G.numVertexes;i++){
lowcost[i] = G.arc[0][i];
adjvex[i] = 0;
}
for(i=1;i<G.numVertexes;i++){
min = INFINITY;
j=1;k=0;
while(j<G.numVertexes){
if(lowcost[j]!=0&&lowcost[j]<min){
min = lowcost[j];
k = j;
}
j++;
}
printf("(%d,%d)\n",adjvex[k],k);
lowcost[k] = 0;
for(j=1;j<G.numVertexes;j++){
if(lowcost[j]!=0&&G.arc[k][j]<lowcost[j]){
lowcost[j] = G.arc[k][j];
adjvex[j] = k;
}
}
}
}
这样就能不断更新两个数组,来判断他们是否在环中。其中是根据lowcost数组来判断,而adjvex数组只是用记录最小生成树。而k的作用就是把一个点其所相连的点放在lowcost数组中。
以下是完整代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#define MAXEDGE 20
#define MAXVEX 20
#define INFINITY 65535
typedef struct
{
int arc[MAXVEX][MAXVEX];
int numVertexes, numEdges;
}MGraph;
typedef struct
{
int begin;
int end;
int weight;
}Edge; //对边集数组Edge结构的定义
//zaotu
void CreateMGraph(MGraph *G)
{
int i, j;
G->numEdges=15;
G->numVertexes=9;
for (i = 0; i < G->numVertexes; i++)//初始化图
{
for ( j = 0; j < G->numVertexes; j++)
{
if (i==j)
G->arc[i][j]=0;
else
G->arc[i][j] = G->arc[j][i] = INFINITY;
}
}
G->arc[0][1]=10;
G->arc[0][5]=11;
G->arc[1][2]=18;
G->arc[1][8]=12;
G->arc[1][6]=16;
G->arc[2][8]=8;
G->arc[2][3]=22;
G->arc[3][8]=21;
G->arc[3][6]=24;
G->arc[3][7]=16;
G->arc[3][4]=20;
G->arc[4][7]=7;
G->arc[4][5]=26;
G->arc[5][6]=17;
G->arc[6][7]=19;
for(i = 0; i < G->numVertexes; i++)
{
for(j = i; j < G->numVertexes; j++)
{
G->arc[j][i] =G->arc[i][j];
}
}
}
//Prim
void prim(MGraph G){
int min,i,j,k;
int adjvex[MAXVEX];
int lowcost[MAXVEX];
lowcost[0] = 0;
adjvex[0] = 0;
for(i=1;i<G.numVertexes;i++){
lowcost[i] = G.arc[0][i];
adjvex[i] = 0;
}
for(i=1;i<G.numVertexes;i++){
min = INFINITY;
j=1;k=0;
while(j<G.numVertexes){
if(lowcost[j]!=0&&lowcost[j]<min){
min = lowcost[j];
k = j;
}
j++;
}
printf("(%d,%d)\n",adjvex[k],k);
lowcost[k] = 0;
for(j=1;j<G.numVertexes;j++){
if(lowcost[j]!=0&&G.arc[k][j]<lowcost[j]){
lowcost[j] = G.arc[k][j];
adjvex[j] = k;
}
}
}
}
int main(){
MGraph G;
CreateMGraph(&G);
prim(G);
return 0;
}
以上就是Prim算法,根据算法代码中的循坏嵌套可以知道其时间复杂度为O(n2);
2. Kruskal
现在我们来换一种思考方式,Prim算法就像是我们去参观一个场馆,从入口进去,先选最近的场馆参加,看完后继续看紧挨的下一个,这样显然不好,那么我们为什么不计划好所有的路线,然后来参观尼。
同样的思路,我们可以先排序,直接找权值最小的边来构建书,但是有时候会形成回路,会多连,此时我们就要用到并查集的思想。
并查集
并查集,是一种判断“远房亲戚”的算法。
打个比方:你身边的某个“朋友”,很有可能就是你父亲的母亲的姑妈的大姨的哥哥的表妹的孙子的女儿的父亲的孙子。如果给定这么一张“家谱”(无向图),如何判断两个顶点是不是“亲戚”呢?用人话说,就是判断一个图中两个点是否联通(两个顶点相互联通则为亲戚)。
并查集是专门用来解决这样的问题的,和搜索不同,并查集在构建图的时候同时就标记出了哪个“人”属于哪个“团伙”(一团伙中的点两两联通)。
为了快速判断两个节点是否在同一个连通块中,克鲁斯卡尔算法使用了并查集。并查集是一种数据结构,它可以维护一个集合的划分,并支持合并和查询两个元素是否在同一个集合中。在克鲁斯卡尔算法中,每个节点可以看做是一个元素,而不同的连通块可以看做是不同的集合,因此可以使用并查集来判断两个节点是否在同一个连通块中,从而快速合并连通块。
算法思路
还是和上面一样的邻接矩阵,克鲁斯卡尔的思想就是站在了上帝视角。先把权值最短的边一个个挑出来。着我们就得使用一个Edges数组来记录。
如下:
typedef struct
{
int begin;
int end;
int weight;
}Edge; //对边集数组Edge结构的定义
我们可以先找到最小边v7和v4,然后再找第二短边,只要他们没有构成回路就将他们入树。
但没这麽简单,如果有回路我们该怎么办。
所以我们得有一个find函数和parent数组来判断是否在一个集合中。
以下是核心代码:
void MiniSpanTree_Kruskal(MGraph G)
{
int i, j, n, m;
int k = 0;
int parent[MAXVEX];// 定义一数组用来判断边与边是否形成环路
Edge edges[MAXEDGE];//定义边集数组,edge的结构为begin,end,weight,均为整型
for ( i = 0; i < G.numVertexes-1; i++)
{
for (j = i + 1; j < G.numVertexes; j++)
{
if (G.arc[i][j]<GRAPH_INFINITY)
{
edges[k].begin = i;
edges[k].end = j;
edges[k].weight = G.arc[i][j];
k++;
}
}
}
sort(edges, &G);
for (i = 0; i < G.numVertexes; i++)
parent[i] = 0;
printf("打印最小生成树:\n");
for (i = 0; i < G.numEdges; i++)
{
n = Find(parent,edges[i].begin);
m = Find(parent,edges[i].end);
if (n != m) //假如n与m不等,说明此边没有与现有的生成树形成环路
{
parent[n] = m; // 将此边的结尾顶点放入下标为起点的parent中。
//表示此顶点已经在生成树集合中
printf("(%d, %d) %d\n", edges[i].begin, edges[i].end, edges[i].weight);
}
}
}
举个例子,从上图的粗线连线可以得到,我们其实是有两个连通的边集合A与B中纳入到最小生成树中的,如图7-6-12所示。当parent[0]=1,表示和v1已经在生成树的边集合A中。此时将parent[0]=1的1改为下标由parent[1]=5表示v1和vs在边集合A中parent[5]=8表示vs与V8在边集合A中,parent[8]=6表示v8与v在边集合A中parent[6]=0表示集合A暂时到头,此时边集合A有oV1、V5、V8V6。我们查看parent 中没有查看的值,parent[2]=8表示2与V8在一个集合中,因此v2也在边集合A中。再由parent[3]=7、parent[4]=7和parent[7]=0可知V3、V4、V7在另一个边集合B中。
而当i=7时,调用find函数你会发现此时n=m,形成了回路,所以v5和v6就不会重复连接。
以下是完整代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#define MAXEDGE 20
#define MAXVEX 20
#define GRAPH_INFINITY 65535
typedef struct
{
int arc[MAXVEX][MAXVEX];
int numVertexes, numEdges;
}MGraph;
typedef struct
{
int begin;
int end;
int weight;
}Edge; //对边集数组Edge结构的定义
//造图
void CreateMGraph(MGraph *G)
{
int i, j;
G->numEdges=15;
G->numVertexes=9;
for (i = 0; i < G->numVertexes; i++)//初始化图
{
for ( j = 0; j < G->numVertexes; j++)
{
if (i==j)
G->arc[i][j]=0;
else
G->arc[i][j] = G->arc[j][i] = GRAPH_INFINITY;
}
}
G->arc[0][1]=10;
G->arc[0][5]=11;
G->arc[1][2]=18;
G->arc[1][8]=12;
G->arc[1][6]=16;
G->arc[2][8]=8;
G->arc[2][3]=22;
G->arc[3][8]=21;
G->arc[3][6]=24;
G->arc[3][7]=16;
G->arc[3][4]=20;
G->arc[4][7]=7;
G->arc[4][5]=26;
G->arc[5][6]=17;
G->arc[6][7]=19;
for(i = 0; i < G->numVertexes; i++)
{
for(j = i; j < G->numVertexes; j++)
{
G->arc[j][i] =G->arc[i][j];
}
}
}
void Swapn(Edge *edges,int i, int j)
{
int temp;
temp = edges[i].begin;
edges[i].begin = edges[j].begin;
edges[j].begin = temp;
temp = edges[i].end;
edges[i].end = edges[j].end;
edges[j].end = temp;
temp = edges[i].weight;
edges[i].weight = edges[j].weight;
edges[j].weight = temp;
}
//对权值进行排序
void sort(Edge edges[],MGraph *G)
{
int i, j;
for ( i = 0; i < G->numEdges; i++)
{
for ( j = i + 1; j < G->numEdges; j++)
{
if (edges[i].weight > edges[j].weight)
{
Swapn(edges, i, j);
}
}
}
printf("权排序之后的为:\n");
for (i = 0; i < G->numEdges; i++)
{
printf("(%d, %d) %d\n", edges[i].begin, edges[i].end, edges[i].weight);
}
}
//查找下标
int Find(int *parent, int f)
{
while ( parent[f] > 0)
{
f = parent[f];
}
return f;
}
//生成最小生成树
void MiniSpanTree_Kruskal(MGraph G)
{
int i, j, n, m;
int k = 0;
int parent[MAXVEX];// 定义一数组用来判断边与边是否形成环路
Edge edges[MAXEDGE];//定义边集数组,edge的结构为begin,end,weight,均为整型
for ( i = 0; i < G.numVertexes-1; i++)
{
for (j = i + 1; j < G.numVertexes; j++)
{
if (G.arc[i][j]<GRAPH_INFINITY)
{
edges[k].begin = i;
edges[k].end = j;
edges[k].weight = G.arc[i][j];
k++;
}
}
}
sort(edges, &G);
for (i = 0; i < G.numVertexes; i++)
parent[i] = 0;
printf("打印最小生成树:\n");
for (i = 0; i < G.numEdges; i++)
{
n = Find(parent,edges[i].begin);
m = Find(parent,edges[i].end);
if (n != m) //假如n与m不等,说明此边没有与现有的生成树形成环路
{
parent[n] = m; // 将此边的结尾顶点放入下标为起点的parent中。
//表示此顶点已经在生成树集合中
printf("(%d, %d) %d\n", edges[i].begin, edges[i].end, edges[i].weight);
}
}
}
int main(void)
{
MGraph G;
CreateMGraph(&G);
MiniSpanTree_Kruskal(G);
return 0;
}
总而言之,两种算法都很优秀,希望大家掌握i。