数据结构-图进阶-最小生成树(Prim && Kruskal)

471 阅读7分钟

前言

图的进阶-最小生成树

最小生成树

最⼩⽣成树:

  • 把构成连通⽹的最⼩代价的⽣成树称为最⼩⽣成树。
  • 百度百科:一个有n个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有n个结点,并且有保持图连通的最少的边。

举个例子

概念都太生硬难懂,上个例子,这个例子是阿里数据结构与算法的面试真题:

假设⽬前有N个顶点,每个顶点连接的路径不⼀样。请你设计⼀个算法,快速找出能覆盖所有顶点的路径.(可使⽤任何编程语⾔实现)

注意:这个问题不是求2点间的最短路径。而是设计一个路线,能够覆盖所有顶点

  • 答案1:此答案不是最优解,只是为了说明面试题的解题方向。图片中粗线是答案路线:

    路径中的权值和:8+12+10+11+17+19+7+16 = 100,这是一种实现方式,但是,不是最优解。通过这个例子应该很好理解上面生硬的概念了。

  • 答案2:最优解方案:

    路径中的权值和:8+12+10+11+16+19+7+16 = 99

生活中也会有很好的例子。例如把上图当做一个村庄,图的顶点当做房子,图的边当做要铺设的网线,而权值是铺设网线的成本。这样和钱联想到一起,是不是感觉这个题就很有意思了。求最小生成树,就是使用最经济实惠的方案给这个村庄铺设网线。

如何求出最小生成树呢?接下来我们要介绍两种算法,Prim算法和Kruskal算法

Prim算法

我们先把图转换成邻接矩阵,如果对邻接矩阵不了解,可以参考我的上一篇文章数据结构-图

利用两个数组来辅助我们记录遍历顶点过程中的数据,两个数组lowcost和arjvex,

  • lowcost用于记录当前顶点与所有与它关联顶点间的权重值。下标:顶点下标,值:与当前顶点的权重。
  • arjvex用于记录当前顶点与哪个顶点相连接。下标:当前顶点下标,值:与当前顶点相连的前一个顶点。
  1. 第一次执行
  2. 第二次执行
  3. 第三次执行
  4. 第四次执行
  5. 第五次执行
  6. ······

代码

#include <stdio.h>
#include "stdlib.h"
#include "math.h"

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

#define MAXEDGE 20
#define MAXVEX 20
#define INFINITYC 65535

typedef int Status;

typedef struct
{
    int arc[MAXVEX][MAXVEX];
    int numVertexes, numEdges;
}MGraph;

void CreateMGraph(MGraph *G) {
    G->numVertexes = 9;
    G->numEdges = 15;
    
    for (int i = 0; i < G->numVertexes; i++) {
        for (int j = 0; j < G->numVertexes; j++) {
            if (i == j) {
                G->arc[i][j] = 0;
            } else {
                G->arc[i][j] = INFINITYC;
            }
        }
    }
    
    G->arc[0][1] = 10;
    G->arc[0][5] = 11;
    G->arc[1][2] = 18;
    G->arc[1][6] = 16;
    G->arc[1][8] = 12;
    G->arc[2][3] = 22;
    G->arc[2][8] = 8;
    G->arc[3][4] = 20;
    G->arc[3][6] = 24;
    G->arc[3][7] = 16;
    G->arc[3][8] = 21;
    G->arc[4][5] = 26;
    G->arc[4][7] = 7;
    G->arc[5][6] = 17;
    G->arc[6][7] = 19;
    
    for (int i = 0; i < G->numVertexes; i++) {
        for (int j = i; j < G->numVertexes; j++) {
            G->arc[j][i] = G->arc[i][j];
        }
    }
}

void minimumSpanningTree(MGraph G) {
    //当前顶点和上个顶点对应关系。下标记录当前顶点,值记录前一个顶点
    int arjvex[MAXVEX] = {0};
    //当前顶点最小花销。下标为当前顶点。值为当前顶点与下一个顶点的最小权值,当值=0时,说明这个顶点已经在最小生成树中。
    int lowcost[MAXVEX] = {INFINITYC};
    //第一个顶点加入到最小生成树中
    int k = 0;
    lowcost[k] = 0;
    //第一个顶点的所有有关系的顶点
    for (int i = 1; i < G.numVertexes; i++) {
        lowcost[i] = G.arc[k][i];
    }
    
    int sum = 0;
    //循环遍历所有顶点
    for (int i = 1; i < G.numVertexes; i++) {
        //找到当前lowcost中的最小值的下标k,
        int min = INFINITYC;
        for (int j = 1; j < G.numVertexes; j++) {
            if (lowcost[j] != 0 && lowcost[j] < min) {
                min = lowcost[j];
                k = j;
            }
        }
        
        //打印前一个顶点到当前顶点的权值
        printf("(V%d, V%d) = %d\n", arjvex[k], k , G.arc[arjvex[k]][k]);
        //求权值的和
        sum += G.arc[arjvex[k]][k];
        //当前顶点在lowcost设置为0,说明已经加入到最小生成树中
        lowcost[k] = 0;
        
        //遍历顶点
        for (int i = 1; i < G.numVertexes; i++) {
            //当前顶点的相关联的顶点权值加入到lowcost中,并更新arjvex中值。
            //lowcost中的等于0的元素 && 这个值大于要加入的权值时,对两个数组进行更新
            if (lowcost[i] != 0 && lowcost[i] > G.arc[k][i]) {
                arjvex[i] = k;
                lowcost[i] = G.arc[k][i];
            }
        }
    }
    //打印结果
    printf("sum=%d\n", sum);
}

运行

int main(int argc, const char * argv[]) {
    printf("Hello, 最小生成树Prim!\n");
    
    MGraph G;
    CreateMGraph(&G);
    minimumSpanningTree(G);
    
    return 0;
}

Kruskal算法

Prim算法的出发点是从顶点开始,而Kruskal算法则是从边开始。

思路

  1. 将邻接矩阵 转化成 边表数组;
  2. 对边表数组根据权值按照从⼩到⼤的顺序排序;
  3. 遍历所有的边, 通过parent 数组找到边的连接信息; 避免闭环问题; 逻辑教育
  4. 如果不存在闭环问题,则加⼊到最⼩⽣成树中. 并且修改parent 数组

边表数组结构如图:

注意,begin小于end

  1. 第一次执行
  2. 第二次执行
  3. 第三次执行
  4. 第四次执行
  5. 第五次执行
  6. 第六次执行
  7. 第七次执行
  8. 第八次执行,此次会出现闭环的情况
  9. 第九次执行
  10. 第十次执行
  11. ......

代码

#include <stdio.h>
#include "stdlib.h"
#include "math.h"

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

#define MAXEDGE 20
#define MAXVEX 20
#define INFINITYC 65535

typedef int Status;

//图结构体
typedef struct
{
    int arc[MAXVEX][MAXVEX];
    int numVertexes, numEdges;
}MGraph;

//边表元素结构体
typedef struct {
    int begin;
    int end;
    int weight;
}Edge;

void CreateMGraph(MGraph *G) {
    G->numVertexes = 9;
    G->numEdges = 15;
    
    for (int i = 0; i < G->numVertexes; i++) {
        for (int j = 0; j < G->numVertexes; j++) {
            if (i == j) {
                G->arc[i][j] = 0;
            } else {
                G->arc[i][j] = INFINITYC;
            }
        }
    }
    
    G->arc[0][1] = 10;
    G->arc[0][5] = 11;
    G->arc[1][2] = 18;
    G->arc[1][6] = 16;
    G->arc[1][8] = 12;
    G->arc[2][3] = 22;
    G->arc[2][8] = 8;
    G->arc[3][4] = 20;
    G->arc[3][6] = 24;
    G->arc[3][7] = 16;
    G->arc[3][8] = 21;
    G->arc[4][5] = 26;
    G->arc[4][7] = 7;
    G->arc[5][6] = 17;
    G->arc[6][7] = 19;
    
    for (int i = 0; i < G->numVertexes; i++) {
        for (int j = i; j < G->numVertexes; j++) {
            G->arc[j][i] = G->arc[i][j];
        }
    }
}

//交换边
void SwapEdge(Edge *edge, int i, int j) {
    int temp;
    
    temp = edge[i].begin;
    edge[i].begin = edge[j].begin;
    edge[j].begin = temp;
    
    temp = edge[i].end;
    edge[i].end = edge[j].end;
    edge[j].end = temp;
    
    temp = edge[i].weight;
    edge[i].weight = edge[j].weight;
    edge[j].weight = temp;
}

//边权值排序
void sortEdges(Edge *edge, MGraph G) {
    for (int i = 0; i < G.numEdges; i++) {
        for (int j = i + 1; j < G.numEdges; j++) {
            if (edge[i].weight > edge[j].weight) {
                SwapEdge(edge, i, j);
            }
        }
    }
    
    printf("边数组排序后的结构:\n");
    for (int i = 0; i < G.numEdges; i++) {
        printf("(%d, %d) = %d\n", edge[i].begin, edge[i].end, edge[i].weight);
    }
}

//查找连线顶点的尾部下标。当前顶点的下标f,在parent中的尾部下标。有助于我们判断是否会出现闭环的情况
int Find(int *parent, int f) {
    while (parent[f] > 0) {
        f = parent[f];
    }
    
    return f;
}

void minimumSpanningTree(MGraph G) {
    //边表
    Edge edge[MAXEDGE];
    //边表赋值
    int k = 0;
    for (int i = 0; i< G.numVertexes - 1; i++) {
        for (int j = i + 1; j < G.numVertexes; j++) {
            if (G.arc[i][j] < INFINITYC) {
                edge[k].begin = i;
                edge[k].end = j;
                edge[k].weight = G.arc[i][j];
                k++;
            }
        }
    }
    //边表排序
    sortEdges(edge, G);
    //顶点关系数组。下标为当前顶点,值为下一个顶点的下标
    int parent[MAXVEX] = {0};
    
    printf("打印最小生成树:\n");
    int sum = 0;
    //遍历边
    for (int i = 0; i < G.numEdges; i++) {
        int n = Find(parent, edge[i].begin);
        int m = Find(parent, edge[i].end);
        
        //如果m=n,会出现闭环
        if (m != n) {
            parent[n] = m;
            
            printf("(V%d, V%d) = %d\n", edge[i].begin, edge[i].end, edge[i].weight);
            sum += edge[i].weight;
        }
    }
    printf("sum = %d\n", sum);
}

运行

int main(int argc, const char * argv[]) {
    printf("Hello, 最小生成树_Kruskal!\n");
    MGraph G;
    CreateMGraph(&G);
    minimumSpanningTree(G);
    return 0;
}