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

一、问题描述

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

在本实验中,我们将讨论解决最小生成树问题的两种算法: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出发到V\A的最短弧的尾结点
 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\n", 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\n", 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为无向图。\n");
134     scanf("%d", &Kind);
135 
136     printf("请输入图的结点数和边数,用空格隔开。\n");
137     scanf("%d%d", &n, &m);
138     G.Init(n, Kind);
139 
140     printf("请输入图中所有的边,格式为空格相隔的3个数,有向图表示“起始点 结束点 边权”,无向图表示“点1 点2 边权”。其中结点的标号范围为[1,n]。\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\n", u, v, Edge[j].w);
 71                     j++;
 72                     break;
 73                 }
 74                 j++;
 75             }
 76         }
 77         printf("最小生成树的权值和为:%d\n", 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("请输入图的结点数和边数,用空格隔开。\n");
 89     scanf("%d%d", &n, &m);
 90     G.Init(n);
 91 
 92     printf("请输入图中所有的边,格式为空格相隔的3个数,表示“点1 点2 边权”。其中结点的标号范围为[1,n]。\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

 

转载于:https://www.cnblogs.com/CQBZOIer-zyy/p/5185412.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值