最小生成树之克鲁斯卡尔(Kruskal)算法

学习最小生成树算法之前我们先来了解下 下面这些概念:

(Tree):如果一个无向连通图中不存在回路,则这种图称为树。

生成树 (Spanning Tree):无向连通图G的一个子图如果是一颗包含G的所有顶点的树,则该子图称为G的生成树。

生成树是连通图的极小连通子图。这里所谓极小是指:若在树中任意增加一条边,则将出现一条回路;若去掉一条边,将会使之变成非连通图。

最小生成树(Minimum Spanning Tree,MST):或者称为最小代价树Minimum-cost Spanning Tree:对无向连通图的生成树,各边的权值总和称为生成树的权,权最小的生成树称为最小生成树。

构成生成树的准则有三条:

<1> 必须只使用该网络中的边来构造最小生成树。

<2> 必须使用且仅使用n-1条边来连接网络中的n个顶点

<3> 不能使用产生回路的边。

构造最小生成树的算法主要有:克鲁斯卡尔(Kruskal)算法和普利姆(Prim)算法他们都遵循以上准则。

接下分别讨论一下这两种算法以及判定最小生成树是否唯一的方法。

克鲁斯卡尔算法

克鲁斯卡尔算法的基本思想是以边为主导地位,始终选择当前可用(所选的边不能构成回路)的最小权植边。所以Kruskal算法的第一步是给所有的边按照从小到大的顺序排序。这一步可以直接使用库函数qsort或者sort。接下来从小到大依次考察每一条边(u,v)。

具体实现过程如下:

<1> 设一个有n个顶点的连通网络为G(V,E),最初先构造一个只有n个顶点,没有边的非连通图T={V,空},图中每个顶点自成一格连通分量。

<2> 在E中选择一条具有最小权植的边时,若该边的两个顶点落在不同的连通分量上,则将此边加入到T中;否则,即这条边的两个顶点落到同一连通分量      上,则将此边舍去(此后永不选用这条边),重新选择一条权植最小的边。

<3> 如此重复下去,直到所有顶点在同一连通分量上为止。

下面是伪代码:

 1 // 把所有边排序,记第i小的边为e[i] (1<=i<=m)m为边的个数
 2 // 初始化MST为空
 3 // 初始化连通分量,使每个点各自成为一个独立的连通分量
 4
 5 for (int i = 0; i < m; i++)
 6 {
 7     if (e[i].u和e[i].v不在同一连通分量)
 8     {
 9         // 把边e[i]加入MST
10         // 合并e[i].u和e[i].v所在的连通分量
11     }
12 } 

上面的伪代码,最关键的地方在于“连通分量的查询合并”,需要知道任意两个点是否在同一连通分量中,还需要合并两个连通分量。

这个问题正好可以用并查集完美的解决(不得不佩服前辈们聪明才智啊!)

并查集(Union-Find set)这个数据结构可以方便快速的解决这个问题。基本的处理思想是:初始时把每个对象看作是一个单元素集合;然后依次按顺序读入联通边,将连通边中的两个元素合并。在此过程中将重复使用一个搜索(Find)运算,确定一个集合在那个集合中。当读入一个连通边(u,v)时,先判断u和v是否在同一个集合中,如果是则不用合并;如果不是,则用一个合并(Union)运算把u、v所在集合合并,使得这两个集合中的任意两个元素都连通。因此并查集在处理时,主要用到搜索合并两个运算。

为了方便并查集的描述与实现,通常把先后加入到一个集合中的元素表示成一个树结构,并用根结点的序号来表示这个集合。因此定义一个parent[n]的数组,parent[i]中存放的就是结点i所在的树中结点i的父亲节点的序号。例如,如果parent[4]=5,就是说4号结点的父亲结点是5号结点。约定:如果i的父结点(即parent[i])是负数,则表示结点i就是它所在的集合的根结点,因为集合中没有结点的序号是负的;并且用负数的绝对值作为这个集合中所含结点的个数。例如,如果parent[7]=-4,说明7号结点就是它所在集合的根结点,这个集合有四个元素。初始时结点的parent值为-1(每个结点都是根结点,只包含它自己一个元素)。

实现Kruskal算法数据结构主要有3个函数。

 1 void UFset() // 初始化
 2 {
 3     for (int i = 0; i < n; i ++)
 4         parent[i] = -1;
 5 }
 6 int Find(int x)  // 查找并返回结点x所属集合的根结点
 7 {
 8     int s;    // 查找位置
 9     for (s = x; parent[s]>=0; s = parent[s]);  // 注意这里的 ;
10     while (s != x)   // 优化方案 -- 压缩路径,使后续的查找
11     {
12         int tmp = parent[x];
13         parent[x] = s;
14         x = tmp;
15     }
16     return s;
17 }
18 // R1和R2是两个元素,属于两个不同的集合,现在合并这两个集合
19 void Union (int R1, int R2)
20 {
21     // r1位R1的根结点,r2位R2的根结点
22     int r1 = Find(R1), r2 = Find(R2);
23     int tmp = parent[r1] + parent[r2];   // 两个集合的结点个数之和(负数)
24     // 如果R2所在树结点个数 > R1所在树结点个数
25     // 注意parent[r1]和parent[r2]都是负数
26     if(parent[r1] > parent[r2])    // 优化方案 -- 加权法则
27     {
28         parent[r1] = r2;        // 将根结点r1所在的树作为r2的子树(合并)
29         parent[r2] = tmp;       // 跟新根结点r2的parent[]值
30     }
31     else
32     {
33         parent[r2] = r1;         // 将根结点r2所在的树作为r1的子树(合并)
34         parent[r1] = tmp;        // 跟新根结点r1的parent[]值
35     }
36 } 

接下来对 Find 函数和 Union 函数的实现过程作详细解释。

Find 函数:在 Find 函数中如果仅仅靠一个循环来直接得到结点所属集合的根结点的话,通过多次的 Union 操作就会有很多结点在树的比较深层次中,再查找起来就会很费时。可以通过压缩路径来加快后续的查找速度:增加一个 While 循环,每次都把从结点 x 到集合根结点的路径上经过的结点直接设置为根结点的子女结点。虽然这增加了时间,但以后的查找会更快。如图 3.4 所示,假设从结点 x = 6 开始压缩路径,则从结点 6 到根结点1 的路径上有 3 个结点:6、10、8,压缩后,这 3 个结点都直接成为根结点的子女结点,如图(b)所示。

并查集:Find函数中的路径压缩

Union 函数:两个集合并时,任一方可做为另一方的子孙。怎样来处理呢,现在一般采用加权合并,把两个集合中元素个数少的根结点做为元素个数多的根结点的子女结点。这样处理有什么优势呢?直观上看,可以减少树中的深层元素的个数,减少后续查找时间。

例如,假设从 1 开始到 n,不断合并第 i 个结点与第 i+1 个结点,采用加权合并思路的过程如下图所示(各子树根结点上方的数字为其 parent[ ]值)。这样查找任一结点所属集合的时间复杂度几乎都是 O(1)!!!

并查集:加权合并

不用加权规则可能会得到下图所示的结果。这就是典型的退化树(只有一个叶结点,且每个非叶结点只有一个子结点)现象,再查找起来就会很费时,例如查找结点 n 的根结点时复杂度为 O(n)。

并查集:合并时不加权的结果。

例 利用 Kruskal 算法求无向网的最小生成树,并输出依次选择的各条边及最终求得的最小生成树的权。

假设数据输入时采用如下的格式进行输入:首先输入顶点个数 n 和边数 m,然后输入 m 条边的数据。每条边的数据格式为:u v w,分别表示这条边的两个顶点及边上的权值。顶点序号从 1开始计起。

分析:

在下面的代码中,首先读入边的信息,存放到数组 edges[ ]中,并按权值从小到大进行排序。

Kruskal( )函数用于实现 :首先初始化并查集,然后从 edges[ ]数组中依次选用每条边,如果这条边的两个顶点位于同一个连通分量,则要弃用这条边;否则合并这两个顶点所在的连通分量。

代码如下:

 1 #include <stdio.h>
 2 #include <string.h>
 3 #include <algorithm>
 4 #define MAXN 11  //顶点个数的最大值
 5 #define MAXM 20  //边的个数的最大值
 6 using namespace std;
 7
 8 struct edge  //边
 9 {
10     int u, v, w; //边的顶点、权值
11 }edges[MAXM]; //边的数组
12
13 int parent[MAXN];  //parent[i]为顶点 i 所在集合对应的树中的根结点
14 int n, m;  //顶点个数、边的个数
15 int i, j;  //循环变量
16 void UFset( )  //初始化
17 {
18     for( i=1; i<=n; i++ )
19         parent[i] = -1;
20 }
21 int Find( int x ) //查找并返回节点 x 所属集合的根结点
22 {
23     int s; //查找位置
24     for( s=x; parent[s]>=0; s=parent[s] );
25     while( s!=x ) //优化方案―压缩路径,使后续的查找操作加速。
26     {
27         int tmp = parent[x];
28         parent[x] = s;
29         x = tmp;
30     }
31     return s;
32 }
33
34 //将两个不同集合的元素进行合并,使两个集合中任两个元素都连通
35 void Union( int R1, int R2 )
36 {
37     int r1 = Find(R1), r2 = Find(R2); //r1 为 R1 的根结点,r2 为 R2 的根结点
38     int tmp = parent[r1] + parent[r2]; //两个集合结点个数之和(负数)
39     //如果 R2 所在树结点个数 > R1 所在树结点个数(注意 parent[r1]是负数)
40     if( parent[r1] > parent[r2] ) //优化方案――加权法则
41     {
42         parent[r1] = r2;
43         parent[r2] = tmp;
44     }
45     else
46     {
47         parent[r2] = r1;
48         parent[r1] = tmp;
49     }
50 }
51 bool cmp( edge a, edge b ) //实现从小到大排序的比较函数
52 {
53     return a.w <= b.w;
54 }
55 void Kruskal( )
56 {
57     int sumweight = 0;  //生成树的权值
58     int num = 0;  //已选用的边的数目
59     int u, v;  //选用边的两个顶点
60     UFset( ); //初始化 parent[]数组
61     for( i=0; i<m; i++ )
62     {
63         u = edges[i].u; v = edges[i].v;
64         if( Find(u) != Find(v) )
65         {
66             printf( "%d %d %d\n", u, v, edges[i].w );
67             sumweight += edges[i].w; num++;
68             Union( u, v );
69         }
70         if( num>=n-1 ) break;
71     }
72     printf( "weight of MST is %d\n", sumweight );
73 }
74 int main( )
75 {
76     int u, v, w; //边的起点和终点及权值
77     scanf( "%d%d", &n, &m ); //读入顶点个数 n
78     for( int i=0; i<m; i++ )
79     {
80     scanf( "%d%d%d", &u, &v, &w ); //读入边的起点和终点
81     edges[i].u = u; edges[i].v = v; edges[i].w = w;
82     }
83     sort(edges,edges+m,cmp);
84     Kruskal();
85     return 0;
86 }

剩下的算法下次更新!!

时间: 2024-10-01 19:05:00

最小生成树之克鲁斯卡尔(Kruskal)算法的相关文章

图的生成树(森林)(克鲁斯卡尔Kruskal算法和普里姆Prim算法)、以及并查集的使用

图的连通性问题:无向图的连通分量和生成树,所有顶点均由边连接在一起,但不存在回路的图. 设图 G=(V, E) 是个连通图,当从图任一顶点出发遍历图G 时,将边集 E(G) 分成两个集合 T(G) 和 B(G).其中 T(G)是遍历图时所经过的边的集合,B(G) 是遍历图时未经过的边的集合.显然,G1(V, T) 是图 G 的极小连通子图,即子图G1 是连通图 G 的生成树. 深度优先生成森林   右边的是深度优先生成森林: 连通图的生成树不一定是唯一的,不同的遍历图的方法得到不同的生成树;从不

图解最小生成树 - 克鲁斯卡尔(Kruskal)算法

我们在前面讲过的<克里姆算法>是以某个顶点为起点,逐步找各顶点上最小权值的边来构建最小生成树的.同样的思路,我们也可以直接就以边为目标去构建,因为权值为边上,直接找最小权值的边来构建生成树也是很自然的想法,只不过构建时要考虑是否会形成环而已,此时我们就用到了图的存储结构中的边集数组结构,如图7-6-7 假设现在我们已经通过邻接矩阵得到了边集数组edges并按权值从小到大排列如上图. 下面我们对着程序和每一步循环的图示来看: 算法代码:(改编自<大话数据结构>) C++ Code 1

克鲁斯卡尔(Kruskal)算法

1 # include <stdio.h> 2 3 # define MAX_VERTEXES 100//最大顶点数 4 # define MAXEDGE 20//边集数组最大值 5 # define INFINITY 65535//代表不可能的数(无穷大) 6 7 typedef struct 8 {//图 结构体定义 9 int arc[MAX_VERTEXES][MAX_VERTEXES];//二位数组 矩阵 10 int numVertexes, numEdges;//当前图中的顶点数

最小生成树之Kruskal(克鲁斯卡尔)算法

克鲁斯卡尔算法:       是在剩下的所有未选取的边中,找最小边,如果和已选取的边构成回路,则放弃,选取次小边. 先构造一个只含 n 个顶点.而边集为空的子图,把子图中各个顶点看成各棵树上的根结点,之后,从网的边集 E 中选取一条权值最小的边, 若该条边的两个顶点分属不同的树,则将其加入子图,即把两棵树合成一棵树,反之,若该条边的两个顶点已落在同一棵树上,则不可取, 而应该取下一条权值最小的边再试之.依次类推,直到森林中只有一棵树,也即子图中含有 n-1 条边为止. 时间复杂度为为O(e^2)

最小生成树( 克鲁斯卡尔算法)

/* Name: Copyright: Author: Date: 01-12-14 20:17 Description: 最小生成树( 克鲁斯卡尔算法) 关于并查集的算法,参见<一种简单而有趣的数据结构--并查集>http://blog.csdn.net/qiaoruozhuo/article/details/39674991 */ #include<stdio.h> #include<stdlib.h> #define MAXN 1000 //最大顶点数量 #def

【图论】【最小生成树】Prim和Kruskal算法

在数据结构的教材上,讲到的图的最小生成树算法有两种,一种是Prim(普利姆)算法,一种是Kruskal(克鲁斯卡尔)算法. 两种算法的生成思路有所不同: Prim算法: 算法思想: 算法思想就是每次找到一个距离生成集合最近的点,加入,然后更新距离剩余点之间的距离,继续迭代. 算法步骤: 1.任意选择一个点作为起点,将该点标记为已加入最小生成树,使用一个数组表示该点已加入,join[i] = 1表示i点已加入集合: 2.将最小生成树集合作为整体,计算到其他未加入该集合的点(jion[i] == 0

最小生成树-Prim算法和Kruskal算法

原文链接:http://www.cnblogs.com/biyeymyhjob/archive/2012/07/30/2615542.html Prim算法 1.概览 普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树.意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex (graph theory)),且其所有边的权值之和亦为最小.该算法于1930年由捷克数学家沃伊捷赫·亚尔尼克(英语:Vojtěch Jarník)发现:并在195

转载:最小生成树-Prim算法和Kruskal算法

本文摘自:http://www.cnblogs.com/biyeymyhjob/archive/2012/07/30/2615542.html 最小生成树-Prim算法和Kruskal算法 Prim算法 1.概览 普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树.意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex (graph theory)),且其所有边的权值之和亦为最小.该算法于1930年由捷克数学家沃伊捷赫·亚尔尼克(英语:

[转载]最小生成树-Prim算法和Kruskal算法

转载地址:http://www.cnblogs.com/biyeymyhjob/archive/2012/07/30/2615542.html 自己在学,感觉这个讲的很不错,就转载了. Prim算法 1.概览 普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树.意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex (graph theory)),且其所有边的权值之和亦为最小.该算法于1930年由捷克数学家沃伊捷赫·亚尔尼克(英语:Vo

最小生成树Prim算法和Kruskal算法

Prim算法(使用visited数组实现) Prim算法求最小生成树的时候和边数无关,和顶点树有关,所以适合求解稠密网的最小生成树. Prim算法的步骤包括: 1. 将一个图分为两部分,一部分归为点集U,一部分归为点集V,U的初始集合为{V1},V的初始集合为{ALL-V1}. 2. 针对U开始找U中各节点的所有关联的边的权值最小的那个,然后将关联的节点Vi加入到U中,并且从V中删除(注意不能形成环). 3. 递归执行步骤2,直到V中的集合为空. 4. U中所有节点构成的树就是最小生成树. 方法