最小生成树算法(普里姆算法和克鲁斯卡尔算法)

什么是生成树?

一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但是只有足以构成一棵树的n-1条边。

理解:

  1. 连通图是属于无向图的范畴,有向图的连通子图叫强连通图
  2. 它含有n个全部顶点,只有n-1条,将n个顶点连起来至少要n-1条边
  3. 少于n-1条边连不起来,那么则无法连通。比如10个点直线连起来至少中间要有9条边
  4. 多于n-1条边会形成环,是连通图,但是不是极小的连通子图。且一棵树肯定是没有环的,多于n-1条边那就不是树了
  5. 必须含有n个顶点,且不能多于n-1条边,也不能少于n-1条边
  6. 故一个连通图的生成树是一个极小的连通子图

最小生成树:

  • 最小生成树来自于无向网。
  • 无向图在边上加上权值就成了无向网。
  • 一个无向图可以有多种不同姿态连接的生成树。
  • 最小生成树就是--各边上权值之和最小的生成树。

本文分别介绍两种算法:普里姆算法(prim)和克鲁斯卡尔算法(Kruska)


1.普利姆算法(prim)

用邻接矩阵存储无向网:

typedef char Vertextype;
typedef int  EdgeType;
#define MAXVEX 100
#define INFINITY 65535
typedef struct
{
    VertexType vexs[MAXVEX];
    EdgeType arc[MAXVEX][MAXVEX];
    int numVertexs,numEdges;
}MGraph;

设N=(V, {E})为一个无向网,V为顶点集,{E}为边集,U={}为空的顶点集,TE={}空的边集

lowcost[maxvwx]:lowcost[i]表示对于V_{i}in U-V,对任意U_{i}in U,从V_{i}U_{i}的边的最小权值; 对于V_{i}in U,lowcost[i]=0
例如:初始化时将V_{0}加入U,则对于V_{0}in U,lowcost[0]=0;若对于V_{i}in U-VU_{j}in U,若存在权值最小为W_{ij}的边(U_{j}V_{i}),则lowcost[i]=W_{ij}

adjvex[maxvex]:  adjvex[i]表示顶点V_{i}由lowcost[i]权值所代表的边连接到U中顶点的位置

例如:若存在lowcost[i]=W_{ij},则adjvex[i]=j

大致算法:

  1. 初始将V_{0}加入U,使U={V_{0}},TE={},k=0,V_{k}=V_{0}
  2. 对于V_{k}in U,更新lowcost[i],adjvex[j]
  3. 从lowcost[i]中选择最小权值的边,将lowcost[i]=0,对于V_{i}in U-V,将V_{i}加入U, adjvex[i]=j,将(U_{j}V_{i})加入到TE
  4. V_{k}=V_{i},重复上述2~4,知道U=V

无向图和相应的邻接矩阵:

 手撸算法图解:

 

代码:

void MiniSpanTree_Prim(MGraph G)
{
    int adjvex[MAXVEX];
    int lowcost[MAXVEX];
    adjvex[0]=0;
    lowcost[0]=0;//lowcost[i]=0,代表此下标的顶点加入生成树,此处将V0加入生成树,进行初始化
    for(int i=1;i<G.numVertexs;i++)
    {
        lowcost[i]=G.arc[0][i];//将V0与Vi有边的权值存入数组,无边的存入无穷大
        adjvex[i]=0;//顶点Vi由权值为lowcost[i]的边连接的另一顶点为V0
    }
    for(int i=1;i<G.numVertexs;i++)//还需要将剩余的G.numVertexs-1个顶点加入生成树
    {
        int min=INFINITY;int k=0;//初始化最小权值为无穷大
        for(int j=1;j<G.numVertexs;j++)//循环全部顶点
        {
            if(lowcost[j]!=0&&lowcost[j]<min)//lowcost[j]=0说明已经在生成树中
            {
                min=lowcost[j];
                k=j;
            }//找到权值最小的边
        }
        printf("将顶点为%d和%d,权为%d的边加入生成树",k, adjvex[k],lowcost[k]);
        lowcost[k]=0;//加入生成树
        for(int j=1;j<G.numVertexs;j++)//更新lowcost[]和adjvex[]
        {    //寻求最小的从V-U到U的边权值,并进行更新
             if(lowcost[j]!=0&&G.arc[k][j]<lowcost[j])//lowcost[j]!=0说明已经进入生成树
             {  //如果lowcost[j]<G.arc[k][j],则对lowcost[j]不进行更新
                lowcost[j]=G.arc[k][j];//否则更新
                adjvex[j]=k;//并更新以j为一端顶点该边的另一顶点为k
             }
        }
    }
   
}

2.克鲁斯卡尔算法(Kruskal)

大致算法思想:假设连通网N=(V,{E}),则令最小生成树的初始状态为只有n个顶点而没有边的非连通图T=(V,{}),图中每个顶点自成一个连通分量。在{E}中选择权值代价最小的边,若该边依附的顶点落在T中的不同的连通分量中,则将此边加入到T中,否则舍弃此边而选择下一条代价最小的边。直到所有的顶点都在同一个连通分量上。

此算法用图的边集数组结构:

typedef struct
{
    int begin;//边开始的顶点位置
    int end;//边结尾的顶点位置
    int weight;//边上的权值
}Edge;

示例无向网及其边集数组:

算法步骤图解:

 

void MiniSpan_Kruskal(MGraph G)
{
    Edge edges[MAXEDGE];//定义边集数组,这里假设已经将邻接矩阵转换成了边集数组,省略此处代码
    int  Same[MAXVEX];//定义Same数组来判断点与点是否属于同一连通分量
    for(int i=0;i<G.numVertexes;i++)
        Same[i]=0;//将Same数组初始化为0
    for(int i=0;i<G.numEdges;i++)
    {   //通过m是否与n相等判断是否begin和end是否属于同一个连通分量
        int n=Find(Same,edges[i].begin);
        int m=Find(Same,edges[i].end);
        if(n!=m){//如果不属于同一个连通分量,则将m串入,相当于将begin和end连接,并且将各自所属的连通分量合并成一个连通分量
          Same[n]=m;
          printf("将代价为%d的(%d,%d)加入生成树",edges[i].weight,edges[i].begin,edges[i].end)      
        }
    }
}
//在Same数组中每一连通分量只有一个出口,肯定存在某个Same[i]=0,但是有多个入口
int Find(int *Same,int f){
    while(Same[f]>0)
        f=Same[f];
    return f;
}//Find函数是为每一个入口寻找出口
//比如为edges[i].begin和edges[i].end入口寻找出口,得到出口分别为m和n
//如果m=n,则出口相同,说明在同一个连通分量
//不相等,则不在同一个连通分量,存在2个不同的出口,需要将一个连通分量的入口连接到另一个连通分量的出口构成一出口,即构成同一个连通分分量

假设对于一个顶点数为n,边数为e的无向网:

普里姆算法是根据顶点来构造最小生成树,通过将一个个顶点加入一个连通分量构成最小生成树,算法复杂度为O(n^2)

克鲁斯卡尔算法是根据不断选择最小权值的边构造生成树,通过判断该边的2个顶点是否属于不同连通分量,若属于不同连通分量,则合并两个不同的连通分量,若属于同一个分量,则舍弃该边,算法复杂度为O(eloge)

适用范围:

普里姆算法针对顶点展开,主要针对稠密图,因为就算稠密图边再多,顶点也就那几个,不能再多

克鲁斯卡尔算法针对边展开,对于稀疏图有很大优势,边数少时效率会非常高

原文地址:https://www.cnblogs.com/zhichao-yan/p/13368499.html