最小生成树-Prim算法&Kruskal算法

这里只是记录两种最小生成树的算法的学习心得,相关概念不做介绍了。

MST(Minimum Spanning Tree, 最小生成树) 有两种解法:

  • 普里姆算法(Prim算法)
    针对于顶点来展开,适用于稠密图,即边数非常多的情况下
  • 克鲁斯卡尔算法(Kruskal)
    针对于边来展开,适用于稀疏图,即边数比较少的情况下

1. Prim算法

1.1 简介

普里姆算法(Prim算法),可以在加权连通图里搜索最小生成树。通过此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,并且所有边的权值之和最小。

1.2 算法简单描述

它是从点的方面考虑构建一棵 MST,大致思想是:设图 G 顶点集合为 V,首先任意选择图 G 中的一点作为起始点 a,将该点加入一个新建的集合 U,再从集合 V-U 中找到另一点 b 使得点b到 U 中的任意一点的权值最小,此时将 b 点也加入集合 U; 以此类推,现在的集合 U={a, b},再从集合 V - U中找到另一点 c 使得c 到 U 中任意一点的权值最小,此时就将 c 点加入集合 U,直到所有顶点全部被加入 U,此时就构建出了一棵 MST。因为有 N 个顶点,所以该 MST 就有 N-1条边,每一次向集合 U 中加入一个点,就意味着找到一条 MST 的边。

1.3 代码介绍

主要难以理解的就是现在的集合 U={a, b},再从集合 V - U中找到另一点 c 使得c 到 U 中任意一点的权值最小这一点如何实现。

#define MAXEDGE 20
#define MAXVEX 20
#define INFINITY 65535

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

// 创建一个图
void CreateMGraph(MGraph *G){
    // 设置边数和顶点数
    G->numVertexes = 9;
    G->numEdges = 15;
    
    // 初始化图都为 INFINITY
    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] = 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(int i = 0; i < G->numVertexes; i++)
    {
        for(int j = i; j < G->numVertexes; j++)
        {
            G->arc[j][i] =G->arc[i][j];
        }
    }
}

// Prim 算法生成最小生成树
void MiniSpanTree_Prim(MGraph G){
    // 1.主要使用了两个一维数组
    int adjvex[MAXVEX];  //
    int lowcost[MAXVEX]; // 存储顶点 Vi 关联的所有边的权值中,最小的权值,lowcost[i] = 0 说明 顶点Vi 已经纳入最小生成树中了
    
    // 2.初始时将 V0 先加入到最小生成树中,也可以选择任意一个顶点作为初始顶点
    for (int i = 0; 0 < G.numVertexes; i++) {
        // 如果你选择顶点  V1 作为初始顶点,此时 lowcost[i] = G.arc[1][i];
        // 因为 lowcost 初始时,存储的是和 V0 顶点有关连边的权值,如果没有关联肯定就是 INFINITY, 想一下邻接矩阵第一行
        lowcost[i] = G.arc[0][i];
        // adjvex 和 lowcost 是对应的, 初始起点是 V0, 所以默认都是 0,如果初始顶点为 V1, 就都是 1 了
        adjvex[i] = 0;
    }
    
    // 关于上面adjvex[i] lowcost[i] 可以这么理解:
    // lowcost[i] 存储顶点 Vi 关联的所有边的权值中,最小的权值,
    // 那么你也得知道这个边的另一个顶点吧,所以就由 adjvex 存储了,adjvex[1] = 0,说明 lowcost[1] 对应的权值所属的边是 (0, 1)
    
    // 3.这个循环是控制次数的,最小生成树的顶点数为 n, 边数为 n - 1,由于已经加入了一个初始顶点 V0, 所以只需要循环 n - 1次
    for (int i = 1; i < G.numVertexes; i++) {
        int k = 0; // 用来记录 lowcost 中最小权值所在的下标
        int j = 1; // 用来控制循环次数
        int Min = INFINITY;   // 用来记录 lowcost 中最小的权值
        
        while (j < G.numVertexes) {
            if (lowcost[j] != 0 && lowcost[j] < Min) {
                Min = lowcost[j];
                k = j;
            }
            j++;
        }
        
        // 此时 k 是lowcost数组中最小权值对应的下标,也就是说这一次循环找到了最小生成树的下一个节点,但是怎么去找到另外一个结点呢?
        // adjvex[] 数组就是用来存储另外一个结点的,adjvex[k] 就是k 顶点关联边中最小权值边的另一个结点了,所以(adjvex[k], k) 为最小生成树的一条边
        printf("(%d, %d)",adjvex[k],k);
        
        // Vk 节点已经入了最小生成树了,所以 lowcost[k] = 0
        lowcost[k] = 0;
        
        // 在最开始提到过,是从 U 中任意节点,找到到 V 中任意节点,权值最小的那个边,
        // 所以lowcost 中的值,lowcost[i] 会逐渐变成邻接矩阵 第 i 列最小的那个值,如果 Vi 已经加入了最小生成树中,那么 lowcost[i] = 0
        // 可以这么理解,初始节点为 V0, 邻接矩阵第一行为{0,10,∞,∞,∞,11,∞, ∞, ∞} lowcost[] 数组中的值对应也是邻接矩阵的第V0行 ,∞ 就是INFINITY,在上面那一步中我们找到和 V0 关联边中最小权值的边权值为 10,边的另一个节点就是 V1, 此时我们会把 V1 加入到 U 中
        // 此时会有一个问题,就是上面那个:是从 U 中任意节点,找到到 V 中任意节点,权值最小的那个边,
        // 所以我们要将 V1 和 V0 的有关边综合起来比较了,此时lowcost[] 存储的是邻接矩阵的第 V0 行,我们现在依次访问邻接矩阵的第 V1 行中的权值,如果相同位置 G.arc[k][index] 下它比 lowcost[index]更小,就替换了lowcost[index] 的权值,同时adjvex[index] = k 存储这个lowcost[index]替换后最小权值的顶点,也就是 (index, k)代表这条边
        // 这里是开始替换 lowcost 和 adjvex 中的值了
        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;
            }
        }
        
        // 此时 k = 1, 并且如果在上面 j = 3 满足 if (lowcost[j] != 0 && G.arc[k][j] < lowcost[j]) 这个条件,并存储了 adjvex[3] = 1, 在接下来的循环中,我们找到了 j = 3 时, lowcost[3]权值最小,说明 V3 是一个顶点,adjvex[3] 也就是 V1 是另一个顶点,因为lowcost[3]对应的权值,是我们访问邻接矩阵 v1 行时替换进去的
    }
}

简要概括:
以某顶点为起点,逐步找个顶点上最小权值的边来构建最小生成树的。
算法的时间复杂度为 O(n²)。

2. Kruskal算法

2.1 简介

克鲁斯卡尔(Kruskal) 算法,是以边为目标去构建,因为权值是在边上的,直接取找最小权值的边来构成生成树也是很自然的想法,只不过在构建的时候需要考虑是否会形成环路,用到了图的存储结构中的边集数组结构。

2.2 算法简单描述

将所有边按照权值的大小进行升序排序,然后从小到大一一判断,条件为:如果这个边不会与之前选择的所有边组成回路,就可以作为最小生成树的一部分;反之,舍去。直到具有 n 个顶点的连通网筛选出来 n-1条边为止。筛选出来的边和所有的顶点构成此连通网的最小生成树。

这里主要的是判断是否会产生回路
在初始状态下给每个顶点赋予不同的标记,对于遍历过程的每条边,其都有两个顶点,判断这两个顶点是否一致,如果一致,说明它们本身就处在一棵树中,如果继续链接就会产生回路;如果不一致,说明它们之间还没有任何关系,可以链接。

2.3 代码如下
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

#define MAXEDGE 20
#define MAXVEX 20
#define INFINITY 65535

typedef int Status;

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

typedef struct{
    int begin;
    int end;
    int weight;
} Edge; // 对于边集数组 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] = 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(int i = 0; i < G->numVertexes; i++)
    {
        for(int 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){
    for (int i = 0; i < G->numEdges; i++) {
        for (int j = i + 1; j < G->numEdges; j++) {
            if (edges[i].weight > edges[j].weight) {
                Swapn(edges, i, j);
            }
        }
    }
    
    printf("权排序之后的为:\n");
    for (int 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 parent[MAXVEX];
    int k = 0;
    Edge edges[MAXEDGE]; // 定义边集数组,edge 的结构为begin, end, weight 均为整型

    // 用来构建边集数组并排序
    for (int i = 0; i < G.numEdges - 1; i++) {
        for(int j = i + 1; j < G.numVertexes; j++){
            if (G.arc[i][j] < INFINITY) {
                edges[k].begin = i;
                edges[k].end = j;
                edges[k].weight = G.arc[i][j];
                k++;
            }
        }
    }
    
    // 排序
    sort(edges, &G);
    
    // 初始化数组值为 0
    for (int i = 0; i < G.numVertexes; i++) {
        parent[i] = 0;
    }
    
    // 打印输出
    printf("打印最小生成树: \n");
    
    int m,n;
    // 对边集数组做遍历
    for (int i = 0; i < G.numEdges; i++) {
        n = Find(parent, edges[i].begin);
        m = Find(parent, edges[i].end);
        
        if (n != m) {
            parent[n] = m;
            printf("(%d, %d) %d\n", edges[i].begin, edges[i].end, edges[i].weight);
        }
    }
}

//
int main(int argc, const char * argv[]) {
    
    MGraph G;
    CreateMGraph(&G);
    MiniSpanTree_Kruskal(G);

    return 0;
}

你可能感兴趣的:(最小生成树-Prim算法&Kruskal算法)