图——最小生成树

最小生成树(Minimum Spanning Tree,简称MST)

  1. 定义
    • 生成树

      一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。

    • 最小生成树
      所有生成树中边的权值之和最小的生成树。
  2. 性质
    1. 是一棵,所以
      • 无回路
      • 如果有 ∣ V ∣ |V| V个顶点,就一定有 ∣ V ∣ − 1 |V|-1 V1条边
    2. 生成树,所以
      • 包含图的全部顶点
      • ∣ V ∣ − 1 |V|-1 V1条边都在图里
    3. 边的权重和最小

  3. 如图:
    图例
    下面这两个都是它的生成树:
    图例的生成树

求一棵树的最小生成树

  • 例:求下图的一棵最小生成树
    图例2

  • 图的表示方法:邻接表

    //图的节点表示
      typedef struct GNode {
          int NV;//顶点数
          int NE;//边数
          WeightType G[MaxVertexNum][MaxVertexNum];//类型为WeightType、大小为MaxVertexNum X MaxVertexNum的二维数组构成邻接矩阵保存边的信息
      }GNode, *MGraph;
    

Prim算法

  1. 步骤:

    1. 任选图中一个顶点,收录进集合最小生成树 M S T MST MST里面
    2. 定义集合 D i s t a n c e [ ] Distance[] Distance[],其中 D i s t a n c e [ V ] Distance[V] Distance[V]表示顶点 V V V到最小生成树 M S T MST MST的最小距离。从未收录进 M S T MST MST的顶点中选一个顶点 V V V,使得 D i s t a n c e [ V ] Distance[V] Distance[V]值最小。
    3. 收录顶点 V V V可能会使得 V V V的未收录的邻接顶点 W W W M S T MST MST的距离改变。所以要刷新 D i s t a n c e [ W ] Distance[W] Distance[W]的值。
    4. 一直重复第二步第三步,直到图中所有顶点都收录进最小生成树中。
  2. 伪代码描述

    void Prim(Graph G,Vertex S)
    {
        MST={S};
        while(1)
        {
            V=未收录顶点中Distance[]值最小的一个;
            if(全部顶点都被收录)
            {
                break;
            }
            Distance[V]=0;  //将V收录进MST
            for(V的每一个邻接点W)
            {
                if(Distance != 0)
                {
                    if(链接顶点V和W的边的权值 < Distance[W])
                    {
                        Distance[W] = 链接顶点V和W的边的权值;
                        Parent[W] = V; //记录顶点W的在生成树中的父顶点
                    }
                }
            }
        }
    }
    
  3. 代码实现
    在未收录的顶点中找出 D i s t e n c e [ ] Distence[] Distence[]值最小的一个,实现方法有两种:一种是使用最小堆实现;另一种是直接用数组排序实现,代码实现类似于有权图的单源最短路径算法。所以下面仅提供第一种方式的代码实现,第二种代码实现可以参考最短路径算法

     //Prim算法
     void Prim(MGraph G, Vertex SourceV)
     {
         MinHeapElementType *Distance = NULL, TempV;
         Vertex *Parent = NULL;
         MinHeap H = NULL;
         Vertex V = 0, AdjV = 0;
         int i = 0;
    
         /******** 申请空间及初始化,其中数组Distance[i]:顶点i到当前生成树的最小距离;Parent[i]:顶点i在生成树中的父节点 ********/
         Distance = (MinHeapElementType *)malloc(sizeof(MinHeapElementType)*G->NV);
         Parent = (Vertex *)malloc(sizeof(Vertex)*G->NV);
         for (i = 0; i < G->NV; i++)
         {
             Distance[i].Distance = INT_MAX;
             Distance[i].Vertex = i;
             Parent[i] = -1;
         }
         Distance[SourceV].Distance = 0;
    
         /************************************** 利用数组Distance里的元素构建最小堆 *****************************************/
         H = BuildMinHeap(Distance, G->NV);
    
         /******************************** Prim算法求生成树 **********************************/
         while (1)
         {
             //当最小堆里没有元素的时候,退出循环
             if (IsMinHeapEmpty(H))
             {
                 break;
             }
             //从最小堆输出最小元素
             TempV = DeleteMinHeap(H);
             V = TempV.Vertex;
             printf("顶点:%d 边权值:%d\n", TempV.Vertex, TempV.Distance);//输出收录的顶点的信息
             Distance[TempV.Vertex].Distance = 0;
    
             //遍历顶点TempV的邻接顶点
             for (AdjV = 0; AdjV < G->NV; AdjV++)
             {
                 //收录顶点TempV的时候,TempV的临界顶点到当前最小生成树的距离可能缩短,检查
                 if ((G->G[V][AdjV] != 0) && (Distance[AdjV].Distance != 0) && (G->G[V][AdjV]) < Distance[AdjV].Distance)
                 {
                     Distance[AdjV].Distance = G->G[V][AdjV];
                     Parent[AdjV] = V;
    
                     //当Temp的邻接顶点到当前最小生成树的距离改变的时候,更新数组Distance中的值和最小堆中的值
                     for (i = 1; i <= H->Size; i++)
                     {
                         if (Distance[AdjV].Vertex == H->Element[i].Vertex)
                         {
                             H->Element[i].Distance = Distance[AdjV].Distance;
                             break;
                         }
                     }
    
                     //最小堆中值改变之后,重新排序最小堆中的元素
                     TempV = H->Element[i];
                     for (; H->Element[i / 2].Distance > TempV.Distance; i = i / 2)
                     {
                         H->Element[i] = H->Element[i / 2];
                     }
                     H->Element[i] = TempV;
                 }
             }
         }
    
         //打印路径表
         printf("\nParent[]:\n");
         for (i = 0; i < G->NV; i++)
         {
             printf("%3d", Parent[i]);
         }
    
         /*************************** 释放空间 *********************************/
         DestroyMinHeap(H);
         free(Distance);
         free(Parent);
     }
    
  4. 运行结果
    Prim算法运行结果

Kruskal算法

  1. 步骤:

    1. 将图中所有边按权值大小进行排序,每次选出权值最小的一条边。
    2. 从未被选择的边中选出权值最小的边。
    3. 如果选出的这条边会使得之前选中的边之中出现回路,就将这条边丢弃,重新选择一条权值最小的边
    4. 重复第二步和第三步,直到选中 ∣ V ∣ − 1 |V|-1 V1条边。
  2. 伪代码描述

    void Kruskal(Graph G)
    {
        MST={};
        while(MST中不到|V|-1条边)
        {
            从边集E中取一条权重最小的边E(v,w); /* 最小堆 */E(v,w)从E中删除;
            if(E(v,w)不在MST中构成回路)   /* 查并集 */
            {E(v,w)加入MST;
            }
            else
            {
                无视E(v,w);
            }
        }
    }
    
    • 关于是否构成回路,可以用并查集来判断。将有边相连的顶点看作是一个集合当中的顶点,当选中一天边准备插入的时候,如果边的两个顶点在同一个集合之中,那么插入这条边之后一定会形成回路。
  3. 代码实现

     //Kruskal算法
     void Kruskal(MGraph G)
     {
         MinHeap H = NULL;
         Edge EdgeSet = NULL, SpanningEdgeSet = NULL;
         ENode EdgeTemp;
         List VertexSet = NULL;
         ListElementType *Temp = NULL;
         int i = 0, j = 0, k = 0;
    
         /*************** 两个集合,EdgeSet存储所有边的信息,SpanningEdgeSet存储生成树边的信息 *****************/
         SpanningEdgeSet= (Edge)malloc(sizeof(ENode)*(G->NV - 1));
         EdgeSet = (Edge)malloc(sizeof(ENode)*(G->NE));
    
         /****************************** 将图中边的信息存储进数组EdgeSet中 ****************************/
         for (i = 0; i < G->NV; i++)
         {
             for (j = i+1; j < G->NV; j++)
             {
                 if (G->G[i][j] != 0)
                 {
                     EdgeSet[k].v1 = i;
                     EdgeSet[k].v2 = j;
                     EdgeSet[k].Weight = G->G[i][j];
                     k++;
                 }
             }
         }
    
         /**************************** 用数组VertexSet存储顶点信息,及每个顶点所属的集合 *************************/
         VertexSet = CreateList();
         Temp = (ListElementType *)malloc(sizeof(ListElementType));//用于暂存数组VertexSet中元素
         for (i = 0; i < G->NV; i++)
         {
             Temp->Data = i;//数据区存储顶点标号
             Temp->Parent = -1;//初始时每个顶点自成一个集合,随着生成树建立时边的确立慢慢将各个集合合并
             AddList(VertexSet, *(Temp));
         }
         free(Temp);
    
         /********************************** 用EdgeSet中的元素建立一个最小堆 *****************************************/
         H = BuildMinHeap(EdgeSet, G->NE);
    
         /****************************************** Kruskal算法 *********************************************/
         for (i = 0; 1;)
         {
             //当收录|V|-1条边的时候,退出循环(生成树的边数 = 顶点数 - 1)
             if (i == (G->NV - 1))
             {
                 break;
             }
    
             //利用最小堆取出权值最小的边
             EdgeTemp = DeleteMinHeap(H);
    
             //利用并查集判断取出的边是否会让当前的生成树产生回路,如果会,继续取下一条最小边
             if (FindRoot(VertexSet, EdgeTemp.v1) != FindRoot(VertexSet, EdgeTemp.v2))
             {
                 SpanningEdgeSet[i] = EdgeTemp;//如果不会产生回路,将边保存在数组SpaningEdgeSet中
                 Union(VertexSet, EdgeTemp.v1, EdgeTemp.v2);//并将该边两顶点所属的两个集合合并
                 i++;
             }
         }
    
         /***************************** 打印生成树的变得集合 *******************************/
         printf("\n生成树的边:\n");
         for (i = 0; i < (G->NV - 1); i++)
         {
             printf("v1=%d  v2=%d  Weight=%d\n", SpanningEdgeSet[i].v1, SpanningEdgeSet[i].v2, SpanningEdgeSet[i].Weight);
         }
    
         /*********************************** 释放空间 ************************************/
         DestroyMinHeap(H);
         DestroyList(VertexSet);
         free(SpanningEdgeSet);
         free(EdgeSet);
     }
    
  4. 运行结果
    Kruskal算法运行结果

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值