最小生成树算法

概念
树(Tree):如果一个无向连通图中不存在回路,则这种图称为树。
n个结点的无向连通图是树,它有几个边吗?
生成树 (Spanning Tree):无向连通图G的一个子图如果是一颗包含G的所有顶点的树,则该子图称为G的生成树。
生成树是连通图的极小连通子图。这里所谓极小是指:若在树中任意增加一条边,则将出现一条回路;若去掉一条边,将会使之变成非连通图。
一个无向连通图中有n个结点,m条边,它有多少个不同形态的生成树呢?
树的属性
树是图的一种特殊形态。图G是树当且仅当以下任意一个条件成立:
1.G有|V|-1条边,无环
2.G有|V|-1条边,连通
3.任意两点之间只有唯一的简单路径
4.G连通,但任意删除一条边后就不连通。
最小生成树(Minimum Spanning Tree,MST):或者称为最小代价树Minimum-cost Spanning Tree:对无向连通图的生成树,各边的权值总和称为生成树的权,权最小的生成树称为最小生成树。
构成生成树的准则有三条
<1> 必须只使用该网络中的边来构造最小生成树。
<2> 必须使用且仅使用n-1条边来连接网络中的n个顶点
<3> 不能使用产生回路的边。
最小边原则
图中权值最小的边(如果唯一的话)一定在最小生成树上
唯一性定理
对于一个图G,如果图中的边权值都不相同,则图中的最小生成树是唯一的,反之不然
克鲁斯卡尔(Kruskal)算法
克鲁斯卡尔算法的基本思想是以边为主导地位,始终选择当前可用(所选的边不能构成回路)的最小权植边。所以Kruskal算法的第一步是给所有的边按照从小到大的顺序排序。这一步可以直接使用库函数qsort或者sort。接下来从小到大依次考察每一条边(u,v)。
具体实现过程如下
<1> 设一个有n个顶点的连通网络为G(V,E),最初先构造一个只有n个顶点,没有边的非连通图T={V,空},图中每个顶点自成一个连通分量。
<2> 在E中选择一条具有最小权植的边时,若该边的两个顶点落在不同的连通分量上,则将此边加入到T中;否则,即这条边的两个顶点落到同一连通分量上,则将此边舍去(此后永不选用这条边),重新选择一条权植最小的边。
<3> 如此重复下去,直到所有顶点在同一连通分量上为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
克鲁斯卡尔(Kruskal)算法
// 把所有边排序,记第i小的边为e[i] (1<=i<=m)m为边的个数
// 初始化MST为空
// 初始化连通分量,使每个点各自成为一个独立的连通分量
for (int i = 0; i < m; i++)
{
    if (e[i].u和e[i].v不在同一连通分量)
    {
        // 把边e[i]加入MST
        // 合并e[i].u和e[i].v所在的连通分量
    }
}
//用并查集来判断加入某一条边是否会出现环。

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

这个问题正好可以用并查集完美的解决。
并查集(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(每个结点都是根结点,只包含它自己一个元素)。

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

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

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

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

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

并查集:加权合并

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

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

假设数据输入时采用如下的格式进行输入:首先输入顶点个数 n 和边数 m,然后输入 m 条边的数据。每条边的数据格式为:u v w,分别表示这条边的两个顶点及边上的权值。顶点序号从 1开始计起。
分析:
在下面的代码中,首先读入边的信息,存放到数组 edges[ ]中,并按权值从小到大进行排序。
Kruskal( )函数用于实现 :首先初始化并查集,然后从 edges[ ]数组中依次选用每条边,如果这条边的两个顶点位于同一个连通分量,则要弃用这条边;否则合并这两个顶点所在的连通分量。
普利姆(Prim)算法
Prim算法是一种产生最小生成树的算法。该算法于1930年由捷克数学家沃伊捷赫·亚尔尼克(英语:Vojtěch Jarník)发现;并在1957年由美国计算机科学家罗伯特·普里姆(英语:Robert C. Prim)独立发现;1959年,艾兹格·迪科斯彻再次发现了该算法。
Prim算法从任意一个顶点开始,每次选择一个与当前顶点集最近的一个顶点,并将两顶点之间的边加入到树中。Prim算法在找当前最近顶点时使用到了贪婪算法。
算法描述:
1. 在一个加权连通图中,顶点集合V,边集合为E
2. 任意选出一个点作为初始顶点,标记为visit,计算所有与之相连接的点的距离,选择距离最短的,标记visit.
3. 重复以下操作,直到所有点都被标记为visit:
在剩下的点中,计算与已标记visit点距离最小的点,标记visit,证明加入了最小生成树。
Boruvka算法
算法描述:
1.对于每个连通块,找到一条由它内部的点连向其它连通块的最小边。
2.把这些最小边加入进去。(注意加入的时候要判它是不是连接了两个不同连通块)
3.如果已经放进去了n-1条边则退出,不然则继续。

 

posted @   心悟&&星际  阅读(663)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App
点击右上角即可分享
微信分享提示