【数据结构学习记录21】——两种最小生成树的算法

一.前言

什么是最小生成树?假设我们有一个连通的网(弧或边带权的图),在这个网里,我们需要构建一个(强)连通分量,且该连通分量的权的和最小。举个例子,就像给你一个镇的地图,在已知的所有路上,新修一条造价最小的村村通。

二.普里姆算法

1.原理

普里姆算法的思想就是,有顶点集U,和已确定的顶点集V。然后:

  1. 从U里任意取一个顶点,然后加入到V中。
  2. 从(U-V)的集合里取一个元素,找一条弧(边)从该元素到V里顶点权最小的,则是我们的弧。
  3. 吧该元素加入V中。
  4. 重复2,3步,直到V=U。
    这样,且除了我们第一个顶点没有权值,其余的点都有权值,刚好是n个顶点,n-1条边(弧),这就是一个连通分量而且是权和最小的连通分量。
    因为普里姆算法和顶点有关系,和边没有关系,所以最适合稠密图。

2.流程

现在放个图作为演示:
在这里插入图片描述

  1. 首先随便选个结点,由于是线性存储的网,所以一般就是A
    在这里插入图片描述
  2. 然后重复在蓝色的点中找一个顶点,且与黄色的顶点中有最短的边:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    然后最终效果:
    在这里插入图片描述

三.克鲁斯卡尔算法

1.原理

刚才我们的普里姆算法的思路是,通过顶点去遍历边,现在我们的克鲁斯卡尔算法是通过边去遍历边。其流程如下:

  1. 将所有边按权的升序排列。
  2. 从最小的边开始遍历,将边的两个顶点标记成相同的值。
  3. 若有一个有值,则给没有值得那个标记为有值的那个顶点的值。
  4. 如果两个 都有值,若值相同则舍去(会变成一个环),若不相同,则都标记为同一个值。
  5. 直到标记完所有的顶点。
    克鲁斯卡尔由于是用边来做计算,而没使用顶点,所以最优适用于稀疏图

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;
        }
    }
}

猜你喜欢

转载自blog.csdn.net/u011017694/article/details/110822131