数据结构图---最小生成树(普里姆算法)

Posted ssyfj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构图---最小生成树(普里姆算法)相关的知识,希望对你有一定的参考价值。

一:最小生成树

(一)定义

我们把构造连通网的最小代价生成树称为最小生成树

(二)什么是最小生成树?

1.是一棵树

1)无回路
2)N个顶点,一定有N-1条边

2.是生成树

1)包含全部顶点
2)N-1条边都在图中

技术分享图片

3.边的权重和最小

(三)案例说明

在实际生活中,我们常常碰到类似这种一类问题:如果要在n个城市之间建立通信联络网,
则连通n个城市仅仅须要n
-1条线路。这时。我们须要考虑这样一个问题。怎样在最节省经费前提 下建立这个通信网.换句话说,我们须要在这n个城市中找出一个包括全部城市的连通子图,使得 其全部边的经费之和最小. 这个问题能够转换为一个图论的问题:图中的每一个节点看成是一个城市, 节点之间的无向边表示修建该路的经费。即每条边都有其对应的权值,而我们的目标是挑选n-1条 边使全部节点保持连通。而且要使得经费之和最小. 这里存在一个显而易见的事实是: 最优解中必定不存在循环(可通过反证法证明). 因此。最后找 出的包括全部城市的连通子图必定没有环路。 这样的连通且没有环路的连通图就简称为。而在一个 连通图中删除全部的环路而形成的树叫做该图的生成树.对于城市建立通信连通网。须要找出的树由 于具有最小的经费之和。因此又被称为最小生成树(Minimum Cost Spanning Tree),简称MST.

二:贪心算法

1.什么是贪?

每一步都要最好(只看下一步)

2.什么是好?

权重最小的边

3.需要约束

1.只能用图里有的边
2.只能正好用掉N-1条边
3.不能有回路

三:普里姆算法(稠密图)

(一)定义

对于一个带权的无向连通图,其每个生成树所有边上的权值之和可能不同,我们把所有边上权值之和最小的生成树称为图的最小生成树。
普里姆算法是以其中某一顶点为起点,逐步寻找各个顶点上最小权值的边来构建最小生成树。
其中运用到了回溯,贪心的思想。

 (二)算法思路

设图G=(V,E),U是顶点集V的一个非空子集。假设(u,v)是一条具有最小权值的边。当中u∈U,v∈V-U,

则必存在一棵包括边(u,v)的最小生成树.

上述的性质能够通过反证法证明。假设(u,v)不包括在G的最小生成树T中。那么,T的路径中必定存

在一条连通U和V-U的边,假设将这条边以(u,v)来替换,我们将获得一个权重更低的生成树,这与T

是最小生成树矛盾.既然MST满足贪婪选择属性。那么。求解最小生成树的问题就简化了非常多。

总结一下,详细的步骤大概例如以下:

1.构建一棵空的最小生成树T。并将全部节点赋值为无穷大.
2.任选一个节点放入T。另外一个节点集合为V-T.
3.对V-T中节点的赋值进行更新(因为此时新增加一个节点,这些距离可能发生变化)
4.从V-T中选择赋值最小的节点,增加T中
5.假设V-T非空,继续步骤3~5,否则算法终结

(三)步骤模拟

原图:

技术分享图片

 

以上图G4为例,来对普里姆进行演示(从第一个顶点A开始通过普里姆算法生成最小生成树)。

技术分享图片

 

初始状态:V是所有顶点的集合,即V={A,B,C,D,E,F,G};U和T都是空! 

第1步:将顶点A加入到U中。 
    此时,U={A}。 
第2步:将顶点B加入到U中。 
    上一步操作之后,U={A}, V-U={B,C,D,E,F,G};因此,边(A,B)的权值最小。将顶点B添加到U中;此时,U={A,B}。 
第3步:将顶点F加入到U中。 
    上一步操作之后,U={A,B}, V-U={C,D,E,F,G};因此,边(B,F)的权值最小。将顶点F添加到U中;此时,U={A,B,F}。 
第4步:将顶点E加入到U中。 
    上一步操作之后,U={A,B,F}, V-U={C,D,E,G};因此,边(F,E)的权值最小。将顶点E添加到U中;此时,U={A,B,F,E}。 
第5步:将顶点D加入到U中。 
    上一步操作之后,U={A,B,F,E}, V-U={C,D,G};因此,边(E,D)的权值最小。将顶点D添加到U中;此时,U={A,B,F,E,D}。 
第6步:将顶点C加入到U中。 
    上一步操作之后,U={A,B,F,E,D}, V-U={C,G};因此,边(D,C)的权值最小。将顶点C添加到U中;此时,U={A,B,F,E,D,C}。 
第7步:将顶点G加入到U中。 
    上一步操作之后,U={A,B,F,E,D,C}, V-U={G};因此,边(F,G)的权值最小。将顶点G添加到U中;此时,U=V。

此时,最小生成树构造完成!它包括的顶点依次是:A B F E D C G

(三)算法实现

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

#define MAXVEX 100    //最大顶点数
#define INFINITY 65535    //用0表示∞

typedef char VertexType;    //顶点类型,字符型A,B,C,D...
typedef int EdgeType;    //边上权值类型10,15,...

//邻接矩阵结构
typedef struct
{
    VertexType vers[MAXVEX];    //顶点表
    EdgeType arc[MAXVEX][MAXVEX];    //邻接矩阵,可看作边表
    int numVertexes, numEdges;    //图中当前的顶点数和边数
}MGraph;


void CreateMGraph(MGraph* G);
void showGraph(MGraph G);

void MiniSpanTree_prim(MGraph G);    //Prim算法生成最小生成树

//Prim算法生成最小生成树
void MiniSpanTree_prim(MGraph G)
{
    int min, i, j, k;
    int adjvex[MAXVEX];        //保存相关顶点下标
    int lowcost[MAXVEX];    //保存相关顶点间边的权值
    lowcost[0] = 0;    //初始化第一个权值为0,即将v0加入生成树
    //lowcost的值为0表示此下标的顶点已经加入生成树
    adjvex[0] = 0;    //初始化第一个顶点下标为0
    for (i = 1; i < G.numVertexes;i++)
    {
        lowcost[i] = G.arc[0][i];    //将v0顶点与之有关的边的权值都存放入权值数组
        adjvex[i] = 0;    //初始化都为v0的下标
    }

    for (i = 1; i < G.numVertexes;i++)
    {
        min = INFINITY;
        j = 1;
        k = 0;
        while (j<G.numVertexes)
        {
            if (lowcost[j]!=0&&lowcost[j]<min)
            {
                //如果权值不为0且权值小于min
                min = lowcost[j];    //则让当前权值成为最小值
                k = j;    //将当前最小值的下标存放k
            }
            j++;
        }
        printf("(%d,%d)", adjvex[k], k);    //打印当前顶点边中权值最小边
        lowcost[k] = 0;//将当前顶点的权值置为0,表示此顶点已经完成任务

        //和上面做了几乎一样的操作,就是更新权值
        for (j = 1; j < G.numVertexes;j++)    //循环所有顶点,因为我们已经确认第一个放入的0,所有我们循环可以省去0
        {
            if (lowcost[j] != 0 && G.arc[k][j]<lowcost[j])    
            {//若下标为k顶点个边权值小于此前顶点,就不会加入生成树权值
                lowcost[j] = G.arc[k][j];    //将较小权值存入lowcost
                adjvex[j] = k;        //将下标为k的顶点存入adjvex
            }
        }
    }
}

int main()
{
    MGraph MG;
    CreateMGraph(&MG);
    showGraph(MG);
    MiniSpanTree_prim(MG);
    system("pause");
    return 0;
}

void CreateMGraph(MGraph* G)
{
    int i, j, k, w;
    G->numVertexes = 9;
    G->numEdges = 15;
    //读入顶点信息
    G->vers[0] = A;
    G->vers[1] = B;
    G->vers[2] = C;
    G->vers[3] = D;
    G->vers[4] = E;
    G->vers[5] = F;
    G->vers[6] = G;
    G->vers[7] = H;
    G->vers[8] = I;

    //getchar();    //可以获取回车符
    for (i = 0; i < G->numVertexes; i++)
        for (j = 0; j < G->numVertexes; j++)
            G->arc[i][j] = INFINITY;    //邻接矩阵初始化

    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][7] = 16;
    G->arc[3][6] = 24;
    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 (k = 0; k < G->numVertexes; k++)    //读入numEdges条边,建立邻接矩阵
    {
        for (i = k; i < G->numVertexes; i++)
        {
            G->arc[i][k] = G->arc[k][i];    //因为是无向图,所有是对称矩阵
        }
    }
}



void showGraph(MGraph G)
{
    for (int i = 0; i < G.numVertexes; i++)
    {
        for (int j = 0; j < G.numVertexes; j++)
        {
            if (G.arc[i][j] != INFINITY)
                printf("%5d", G.arc[i][j]);
            else
                printf("    0");
        }
        printf("
");
    }
}

技术分享图片

(四)普里姆代码分析

//Prim算法生成最小生成树
void MiniSpanTree_prim(MGraph G)
{
    int min, i, j, k;
    int adjvex[MAXVEX];        //保存相关顶点下标
    int lowcost[MAXVEX];    //保存相关顶点间边的权值
    lowcost[0] = 0;    //初始化第一个权值为0,即将v0加入生成树
    //lowcost的值为0表示此下标的顶点已经加入生成树
    adjvex[0] = 0;    //初始化第一个顶点下标为0
    for (i = 1; i < G.numVertexes;i++)
    {
        lowcost[i] = G.arc[0][i];    //将v0顶点与之有关的边的权值都存放入权值数组
        adjvex[i] = 0;    //初始化都为v0的下标
    }

    for (i = 1; i < G.numVertexes;i++)
    {
        min = INFINITY;
        j = 1;
        k = 0;
        while (j<G.numVertexes)
        {
            if (lowcost[j]!=0&&lowcost[j]<min)
            {
                //如果权值不为0且权值小于min
                min = lowcost[j];    //则让当前权值成为最小值
                k = j;    //将当前最小值的下标存放k
            }
            j++;
        }
        printf("(%d,%d)", adjvex[k], k);    //打印当前顶点边中权值最小边
        lowcost[k] = 0;//将当前顶点的权值置为0,表示此顶点已经完成任务

        //和上面做了几乎一样的操作,就是更新权值
        for (j = 1; j < G.numVertexes;j++)    //循环所有顶点,因为我们已经确认第一个放入的0,所有我们循环可以省去0
        {
            if (lowcost[j] != 0 && G.arc[k][j]<lowcost[j])    
            {//若下标为k顶点个边权值小于此前顶点,就不会加入生成树权值
                lowcost[j] = G.arc[k][j];    //将较小权值存入lowcost
                adjvex[j] = k;        //将下标为k的顶点存入adjvex
            }
        }
    }
}

下面是我们要处理的邻接矩阵

技术分享图片

1.创建两个数组,一个存放顶点,一个存放相关顶点间边的权值

int adjvex[MAXVEX];        //保存相关顶点下标
int lowcost[MAXVEX];    //保存相关顶点间边的权值

作用:

技术分享图片

adjvex数组:将存放我们左侧的顶点下标
lowcost数组:将存放我们对应顶点的各个边的权值

会利用这两个来打印出我们所需要的最小边

printf("(%d,%d)", adjvex[k], k); //打印当前顶点边中权值最小边   
其中adjvex[k]存放的是我们左侧的弧尾,k是我们找的的邻接点权值最小的弧头

注意:

比如我们要找v3顶点作为弧头的边,那么我们adjvex[3]中将会存放其弧尾,也就是我们的左侧下标
那么我们去找第一条边时,我们只知道与v0相邻顶点间边的权值,并不知道k值,所以我们开始无法知道adjvex[k]=0中k是谁。
但是我们可以在开始对顶点数组adjvex进行初始化,全部初始化为0,就可以解决这个问题
    lowcost[0] = 0;    //初始化第一个权值为0,即将v0加入生成树
    //lowcost的值为0表示此下标的顶点已经加入生成树
    adjvex[0] = 0;    //初始化第一个顶点下标为0
    for (i = 1; i < G.numVertexes;i++)
    {
        lowcost[i] = G.arc[0][i];    //将v0顶点与之有关的边的权值都存放入权值数组
        adjvex[i] = 0;    //初始化都为v0的下标
    }
其中lowcost[0] = 0;是因为我们开始就将v0点放入生成树中,所以要将对应的lowcost[0]设置为0,我们会在后面,将所有的放入生成树中的顶点全部设置为0,但是注意生成树在代码中不是直接出现的
其中lowcost[i] = G.arc[0][i];  是将对应的邻接顶点的权值放入lowcost中

技术分享图片

2.循环所有的左侧顶点,获取他们相关的最小邻接边

    for (i = 1; i < G.numVertexes;i++)
    {
        min = INFINITY;
        j = 1;
        k = 0;
        while (j<G.numVertexes)
        {
            if (lowcost[j]!=0&&lowcost[j]<min)
            {
                //如果权值不为0且权值小于min
                min = lowcost[j];    //则让当前权值成为最小值
                k = j;    //将当前最小值的下标存放k
            }
            j++;
        }
        printf("(%d,%d)", adjvex[k], k);    //打印当前顶点边中权值最小边
        lowcost[k] = 0;//将当前顶点的权值置为0,表示此顶点已经完成任务

        //和上面做了几乎一样的操作,就是更新权值
        for (j = 1; j < G.numVertexes;j++)    //循环所有顶点,因为我们已经确认第一个放入的0,所有我们循环可以省去0
        {
            if (lowcost[j] != 0 && G.arc[k][j]<lowcost[j])    
            {//若下标为k顶点个边权值小于此前顶点,就不会加入生成树权值
                lowcost[j] = G.arc[k][j];    //将较小权值存入lowcost
                adjvex[j] = k;        //将下标为k的顶点存入adjvex
            }
        }
    }

其中while循环是获取我们的权值中最小的那个的弧头下标,将会和弧尾组成一条边:

        while (j<G.numVertexes)
        {
            if (lowcost[j]!=0&&lowcost[j]<min)
            {
                //如果权值不为0且权值小于min
                min = lowcost[j];    //则让当前权值成为最小值
                k = j;    //将当前最小值的下标存放k
            }
            j++;
        }

下面的权值都会存在lowcost中

技术分享图片

           技术分享图片

    printf("(%d,%d)", adjvex[k], k);  //可以打印处这条边

我们将找到的这个顶点和上面初始时设置的lowcost一样设置为0,表示已经加入生成树,我们不必去修改他们

    lowcost[k] = 0;//将当前顶点的权值置为0,表示此顶点已经完成任务

下面的for循环和我们之前的for循环更新权值是一致的,但是有些不同

        //和上面做了几乎一样的操作,就是更新权值
        for (j = 1; j < G.numVertexes;j++)    //循环所有顶点,因为我们已经确认第一个放入的0,所有我们循环可以省去0
        {
            if (lowcost[j] != 0 && G.arc[k][j]<lowcost[j])    
            {//若下标为k顶点个边权值小于此前顶点,就不会加入生成树权值
                lowcost[j] = G.arc[k][j];    //将较小权值存入lowcost
                adjvex[j] = k;        //将下标为k的顶点存入adjvex
            }
        }

首先我们做了比较

if (lowcost[j] != 0 && G.arc[k][j]<lowcost[j])   
首先顶点不能及时生成树中的,即lowcost[j]!=0,然后其弧权值需要比原来的权值小才行,因为可能出现原来的权值更加小,这是就要选择原来的边作为新的路径

技术分享图片

                lowcost[j] = G.arc[k][j];    //将较小权值存入lowcost
                adjvex[j] = k;        //将下标为k的顶点存入adjvex
我们更新了权值最新值,会在下一次的循环中再次选取下一个点

 















以上是关于数据结构图---最小生成树(普里姆算法)的主要内容,如果未能解决你的问题,请参考以下文章

数据结构之最小生成树(普里姆算法)

挑战程序设计竞赛(算法和数据结构)——13.2最小生成树(普里姆)的JAVA实现

急!数据结构最小生成树prim算法C语言实现

急!数据结构最小生成树prim算法C语言实现

数据结构最小生成树之普里姆(Prim)算法和克鲁斯卡尔(Kruskal)算法

大话数据结构C语言45 最小生成树(普里姆算法)