• [数据结构]最小生成树的实现


    一、问题描述

    在电子电路设计中,我们常常需要将多个组件连接在一起,显然我们希望所用的线能够最短,由此引出最小生成树问题。

    在本实验中,我们将讨论解决最小生成树问题的两种算法:Prim算法和Kruskal算法。其中Prim算法的时间复杂度为O(N^2),如果使用二叉堆来优化寻找新加入的结点,则可以将时间复杂度降到O(E logN),如果使用斐波那契堆,时间复杂度将改善为O(E+N logN);Kruskal算法的时间复杂度为O(E logE)。

    本实验根据算法的不同,对图的存储方式也是不一样的。在Prim算法中,使用邻接表存储图;而在Kruskal算法的实现中,考虑到实现的方便,直接存储了边的相关信息。具体实现下面将会给出。

    二、数据结构——邻接表

    1、邻接表(Prim算法)

    邻接表是图的一种链式存储结构。首先有一个包含所有结点的顺序表,顺序表的每个元素对应一个单链表,表示从该结点出发的弧。每个结点由3部分组成:指针域,指向从当前结点出发的下一条弧的尾结点;尾结点标号;数据域,保存当前弧的信息,比如权重等等。

    需要说明的是,这适用于有向图与无向图,不妨认为无向图的边u--v就是有向图的两条弧u->v与v->u。

    2、另一种存储图的方式(Kruskal算法)

    由Kruskal算法的特点,为了方便找边,我们可以直接用一个“集合”(可以用静态查找表实现,这里的集合的意思是边之间没有逻辑关系)保存所有的边。“集合”中的每个结点由3部分组成:结点1、结点2和当前边的信息(比如权重)。

    可以看出,这样的方式更适合于存储无向图,且对点的信息的访问需求不大,否则将会增大查询的时间复杂度。

    三、算法的设计和实现

    1、Prim算法

    (1)简述

    Prim算法的工作原理与解决单源点最短路的Dijkstra算法相似。我们定义一个集合A,在集合A里面的所有结点已经形成了一棵最小生成树;然后不断向其添加新结点,使得集合A的性质保持不变,直到网络中的所有结点加入到集合A中,算法终止;此时,得到一棵最小生成树。

    本策略属于贪心策略,保证每次添加的结点所对应的新边是权重增加得最小的。

    (2)伪代码

     1 Prim(G, w, x)
     2     for each u∈G.V
     3         u: cost = 4         u: father = NULL
     5     x: cost = 0
     6     for k: 2 to n
     7         u = MinCost(A) //表示从A出发到VA的最短弧的尾结点
     8         for each v∈G.adj[u]
     9             if v∈G && w(u, v) < v: cost
    10                 v: father = u
    11                 v: cost = w(u, v)
    12                 Add v into A

    (3)时间复杂度分析

    由伪代码可以看出,Prim算法的时间复杂度为O(n^2)。

    为了减小算法的时间复杂度,需要找一种更高效的选择新边的方法。由于是寻找最小的边,可以维护一个优先队列,队列排序的关键字为cost,即集合A中的结点到达某个点的最小花费。我们约定,如果不存在这样的边,则v: cost = ∞;集合A中的结点有v: cost = 0。此时,算法的运行时间取决于优先队列的实现方式。

    如果用二叉堆来实现,算法的总时间代价为O(E logN);如果使用斐波拉契堆来实现,算法的运行时间将改进到O(E + N logN)。

    2、Kruskal算法

    (1)简述

    Kruskal算法也属于贪心算法。每次选择一条连接两个不同的连通分量的最短的边,加入到森林,直到添加了n-1条边,得到一棵树,就是需要找的最小生成树。

    (2)伪代码

    1 Kruskal(G, w)
    2     A =3     for each v∈G.V
    4         Make_Set(v)
    5     将G.E的边不递减排序
    6     for each edge(u, v)∈G.E //按照不递减顺序访问,直到加入n-1条边
    7         if Find_Set(u) != Find_Set(v) //两结点不在同一连通分量
    8             Add (u, v) into A
    9             Union(u, v) //合并两个连通分量

    (3)时间复杂度分析

    排序可以使用快排,时间复杂度为O(E logE)。合并两个连通分量时,可以使用并查集且使用路径压缩的方式优化,总运行时间为O((N + E)α(N))。所以Kruskal算法的时间复杂度为O(E logE)。

    3、MST性质及其证明

    (1)MST性质:设G=(N,E)是一个在边E上定义了实数值权重函数w的连通无向图。设集合A为E的一个子集,且A包括在图G的某棵最小生成树中,设(S,N-S)是图G中尊重集合A的任意一个切割(如果一条边(u,v)∈E的一个端点位于集合S,另一个断点位于集合N-S,则称该条边横跨切割(S,N-S)。如果集合A中不存在横跨该切割的边,则称该切割尊重集合A),又设(u,v)是横跨切割(S,N-S)的一条轻量级边。那么边(u,v)对于集合A是安全的(对于(u,v),如果加入集合A后,使得A不违反循环不变式,即A∪{(u,v)}也是某棵最小生成树的子集,则称(u,v)为集合A的安全边)。

    (2)证明:(证明参考《算法导论》)

    设T是一棵包括A的最小生成树,假设T不包含轻量级边(u,v)。

    边(u,v)与T中从结点u到结点v的简单路径p形成一个环路,如下图。由于结点u和结点v分别处在切割(S,N-S)的两端,T中至少有一条边属于简单路径p并且横跨该切割。设(x,y)为这样的一条边。因为切割(S,N-S)尊重集合A,故边(x,y)不在集合A中。由于边(x,y)位于T中从u到v的唯一简单路径上,将该条边删除会导致T被分解为两个连通分量。将(u,v)加上去可以将这两个连通分量连接起来,形成一棵新的生成树T’=T{(x,y)}∪{(u,v)}。

    如图,黑色结点位于集合S里,白色结点位于集合N-S里。图中仅描述了最小生成树T中的边,而没有绘出图G中的其他边。集合A中的边都为灰色粗边,边(u,v)是横跨(S,N-S)的一条轻量级边。边(x,y)是树T里面从结点u到结点v的唯一简单路径上的一条边。要形成一棵包含(u,v)的最小生成树T’,只需要在T中删除边(x,y),然后加上边(u,v)即可。

    下证T’为一棵最小生成树。

    由于边(u,v)是横跨切割(S,N-S)的一条轻量级边,且(x,y)也横跨该切割,所以w(u,v)<=w(x,y)。因此,w(T’) = w(T) - w(x,y) + w(u,v) <= w(T)。又T是一棵最小生成树,从而有w(T) = w(T’),故T’也是一棵最小生成树。

    下证边(u,v)对于集合A是安全的。

    由于A包含于T,且(x,y)∉A,所以A包含于T’;因此A∪{(u,v)}包含于T’。由于T’是最小生成树,故边(u,v)对于集合A是安全的。证毕。

    四、预期结果和实验中的问题

    1、预期结果

    程序可以正确地找出图中的一棵最小生成树。

    当最小生成树唯一存在时,所找到的最小生成树是唯一的;当最小生成树存在不唯一时,算法将会找出其中一棵生成树,当然程序每次运行所找出的最小生成树都是相同的。

    2、实验中的问题及思考

    (1)次小生成树

    a)定义:设G=(N,E)为一连通无向图,其权重函数为w,假定|E| >= |N|并且所有的权重都互不相同。设τ为G的所有生成树的集合,T’为G的一棵最小生成树,T是一棵生成树,若满足w(T) = min{w(T’’)},T’’∈τT’,则称T是一棵次小生成树。

    b)还没想出除了暴力搜索以外的算法。

    (2)瓶颈生成树

    a)定义:无向图G的瓶颈生成树T是G的一棵生成树,其最大边的权重是G的所有生成树中最小的。

    b)可以发现,最小生成树都是瓶颈生成树,而瓶颈生成树不一定都是最小生成树。

    附:c++源代码:

    1、Prim算法实现

      1 /*
      2 项目:最小生成树——Prim算法
      3 作者:张译尹
      4 */
      5 #include <iostream>
      6 #include <cstdio>
      7 #include <cstring>
      8 
      9 using namespace std;
     10 #define MaxN 120 //结点数
     11 
     12 //结点 
     13 struct node
     14 {
     15     int v, w;
     16     node *next;
     17 };
     18 
     19 //
     20 class My_graph
     21 {
     22 private:
     23     //NodeList L;
     24     node *adj[MaxN]; //单链表头指针
     25     int len; //链表长度(结点个数)
     26 
     27     int VexNum, ArcNum; //图的结点数和边数
     28     int Kind; //图的种类:0-无向图,1-有向图
     29 public:
     30     void Init(int vNum, int kind)
     31     {
     32         VexNum = vNum;
     33         ArcNum = 0;
     34         Kind = kind;
     35 
     36         //L.Init();
     37         memset(adj, 0, sizeof(adj));
     38         len = 0;
     39     }
     40     void D_Addedge(int u, int v, int w) //u->v,权值为w。有向图
     41     {
     42         node *p = new node;
     43         p -> v = v;
     44         p -> w = w;
     45         p -> next = adj[u];
     46         adj[u]= p;
     47     }
     48     void UD_Addedge(int u, int v, int w) //u->v,权值为w。无向图
     49     {
     50         node *p = new node;
     51         p -> v = v;
     52         p -> w = w;
     53         p -> next = adj[u];
     54         adj[u]= p;
     55 
     56         p = new node;
     57         p -> v = u;
     58         p -> w = w;
     59         p -> next = adj[v];
     60         adj[v]= p;
     61     }
     62     void Addedge(int u, int v, int w)
     63     {
     64         if(Kind == 0)
     65             UD_Addedge(u, v, w);
     66         else
     67             D_Addedge(u, v, w);
     68         ArcNum++;
     69     }
     70     int Get_VexNum()
     71     {
     72         return VexNum;
     73     }
     74     void Prim()
     75     {
     76         int i, j;
     77         int n = VexNum, Ans = 0;
     78         int cost[MaxN], MinCost;
     79         int fa[MaxN], v, x;
     80         bool vis[MaxN];
     81         memset(cost, 0x3f, sizeof(cost));
     82         memset(vis, false, sizeof(vis));
     83 
     84         cost[1] = 0; //选择了结点1
     85         vis[1] = true;
     86         fa[1] = 1;
     87         for(node *p =adj[1]; p != NULL; p = p -> next)
     88         {
     89             cost[p -> v] = p -> w;
     90             fa[p -> v] = 1;
     91         }
     92 
     93         for(i = 2; i <= n; i++) //寻找第i个结点 
     94         {
     95             MinCost = 0x3f;
     96             v = -1;
     97             for(j = 1; j <= n; j++)
     98             {
     99                 if(!vis[j] && cost[j] < MinCost)
    100                 {
    101                     v = j;
    102                     MinCost = cost[j];
    103                 }
    104             }
    105             if(v != -1)
    106             {
    107                 vis[v] = true; //将v加入
    108                 printf("(%d %d): %d
    ", fa[v], v, MinCost);
    109 
    110                 Ans += MinCost;
    111                 for(node *p = adj[v]; p != NULL; p = p -> next)
    112                 {
    113                     x = p -> v;
    114                     if(!vis[x] && cost[x] > (p -> w))
    115                     {
    116                         cost[x] = p -> w;
    117                         fa[x] = v;
    118                     }
    119                 }
    120             }
    121         }
    122         printf("最小生成树的权值和为:%d
    ", Ans);
    123     } //Prim 
    124 };
    125 
    126 void Read_and_Build(My_graph &G)
    127 {
    128     int n, m, Kind;
    129     int u, v, w;
    130     int i;
    131     //My_graph G;
    132 
    133     printf("请输入图的类型:1为有向图,0为无向图。
    ");
    134     scanf("%d", &Kind);
    135 
    136     printf("请输入图的结点数和边数,用空格隔开。
    ");
    137     scanf("%d%d", &n, &m);
    138     G.Init(n, Kind);
    139 
    140     printf("请输入图中所有的边,格式为空格相隔的3个数,有向图表示“起始点 结束点 边权”,无向图表示“点1 点2 边权”。其中结点的标号范围为[1,n]。
    ");
    141     for(i = 1; i <= m; i++)
    142     {
    143         scanf("%d%d%d", &u, &v, &w);
    144         G.Addedge(u, v, w);
    145     }
    146 }
    147 
    148 int main()
    149 {
    150     My_graph G;
    151     Read_and_Build(G);
    152     G.Prim();
    153     return 0;
    154 }
    View Code

    2、Kruskal算法实现

      1 /*
      2 项目:最小生成树——Kruskal算法
      3 作者:张译尹
      4 */
      5 #include <iostream>
      6 #include <cstdio>
      7 #include <cstring>
      8 #include <algorithm>
      9 
     10 using namespace std;
     11 #define MaxN 120 //结点数
     12 #define MaxM 10020 //边数
     13 
     14 //
     15 struct E
     16 {
     17     int u, v, w;
     18 };
     19 
     20 class My_graph
     21 {
     22 private:
     23     E Edge[MaxM]; //
     24 
     25     int VexNum, ArcNum; //图的结点数和边数
     26 public:
     27     void Init(int vNum)
     28     {
     29         VexNum = vNum;
     30         ArcNum = 0;
     31         memset(Edge, 0, sizeof(Edge));
     32     }
     33     void Addedge(int u, int v, int w) //u->v,权值为w
     34     {
     35         ArcNum++;
     36         Edge[ArcNum].u = u;
     37         Edge[ArcNum].v = v;
     38         Edge[ArcNum].w = w;
     39     }
     40     static bool Cmp(E a, E b)
     41     {
     42         return (a.w) < (b.w);
     43     }
     44     int Find(int x, int fa[])
     45     {
     46         if(fa[x]==-1)
     47             return x;
     48         return fa[x]=Find(fa[x], fa);
     49     }
     50     void Kruskal()
     51     {
     52         sort(Edge + 1, Edge + ArcNum + 1, Cmp);
     53         int i, j = 1;
     54         int u, v;
     55         int fa[MaxN];
     56         int Ans = 0;
     57         memset(fa, -1, sizeof(fa));
     58         for(i = 1; i <= VexNum - 1; i++)
     59         {
     60             while(j <= ArcNum)
     61             {
     62                 u = Edge[j].u; u = Find(u, fa);
     63                 v = Edge[j].v; v = Find(v, fa);
     64                 if(u != v)
     65                 {
     66                     if(u > v)
     67                         swap(u, v);
     68                     fa[v] = u;
     69                     Ans += Edge[j].w;
     70                     printf("(%d,%d):%d
    ", u, v, Edge[j].w);
     71                     j++;
     72                     break;
     73                 }
     74                 j++;
     75             }
     76         }
     77         printf("最小生成树的权值和为:%d
    ", Ans);
     78     } //Kruskal
     79 };
     80 
     81 void Read_and_Build(My_graph &G)
     82 {
     83     int n, m;
     84     int u, v, w;
     85     int i;
     86     //My_graph G;
     87 
     88     printf("请输入图的结点数和边数,用空格隔开。
    ");
     89     scanf("%d%d", &n, &m);
     90     G.Init(n);
     91 
     92     printf("请输入图中所有的边,格式为空格相隔的3个数,表示“点1 点2 边权”。其中结点的标号范围为[1,n]。
    ");
     93     for(i = 1; i <= m; i++)
     94     {
     95         scanf("%d%d%d", &u, &v, &w);
     96         G.Addedge(u, v, w);
     97     }
     98 }
     99 
    100 int main()
    101 {
    102     My_graph G;
    103     Read_and_Build(G);
    104     G.Kruskal();
    105     return 0;
    106 }
    View Code
  • 相关阅读:
    生命的等级
    一个笑话
    第一天的日记
    接吻鱼的秘密
    [转载] 女人到底想要什么??
    纪念一塌糊涂bbs
    上海市区找到一处比较便宜的打乒乓地方:)
    朋友在奔驰公司,给我提供的F1照片,奔驰必胜!
    前台mm何处有?
    一个令我感动的女孩!
  • 原文地址:https://www.cnblogs.com/CQBZOIer-zyy/p/5185412.html
Copyright © 2020-2023  润新知