算法学习:最小生成树

最小生成树是无向图中的一个问题,很常见。
在无向图中,连通而且不含有圈(环路)的图称为树。最小生成树(MST)的基本模型可以用下面的题目描述:

题目描述
某省调查乡村交通状况,得到的统计表中列出了任意两村庄间的距离。省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可),并要求铺设的公路总长度为最小。请计算最小的公路总长度。

输入
测试输入包含若干测试用例。每个测试用例的第1行给出村庄数目N ( < 100 );随后的N(N-1)/2行对应村庄间的距离,每行给出一对正整数,分别是两个村庄的编号,以及此两村庄间的距离。为简单起见,村庄从1到N编号。
当N为0时,输入结束,该用例不被处理。

输出
对每个测试用例,在1行里输出最小的公路总长度。

样例输入
8
1 2 42
1 3 68
1 4 35
1 5 1
1 6 70
1 7 25
1 8 79
2 3 59
2 4 63
2 5 65
2 6 6
2 7 46
2 8 82
3 4 28
3 5 62
3 6 92
3 7 96
3 8 43
4 5 28
4 6 37
4 7 92
4 8 5
5 6 3
5 7 54
5 8 93
6 7 83
6 8 22
7 8 17
0
样例输出
82

图的两个基本元素是点和边,与此对应,有两种方法可以构造最小生成树T。这两种算法都基于贪心法,因为MST问题满足贪心法的“最优性原理”,即全局最优包含局部最优。prim算法的原理是“最近的邻居一定在MST上”,kruskal算法的原理是“最短的边一定在MST上”。
(1)prim算法:对点进行贪心操作。从任意一个点u开始,把距离它最近的点v加入到T中;下一步,把距离{u,v}最近的点w加入到T中,继续这个过程,直到所有点都在T中。
(2)kruskal算法:对边进行贪心操作。从最短的边开始,把它加入到T中;在剩下的边中找最短的边,加入到T中;继续这个过程,直到所有点都在T中。
在这两个算法中,重要的问题是判断圈。最小生成树显然不应该有圈,否则就不是“最小”了。所以,在新加入一个点或者边的时候要同时判断是否形成了圈。

1、prim算法
设最小生成树的点的集合是U,开始时最小生成树为空,所以U为空。
在这里插入图片描述
(1)任取一点,例如点1,放到U中,U={1}。
(2)找离集合U中的点最近的邻居,即1的邻居,是2,放到U中,U={1,2}。
(3)找离U最近的点,是5,U={1,2,5}。
(4)距离U最近的点是5,但是它没有扩展新的点,不符合要求。
(5)加入4,U={1,2,5,4}。
(6)加入3,U={1,2,5,4,3}。所有的点都在U中,结束。
上面的步骤和Dijkstra算法的步骤非常相似,不同的是Dijkstra需要更新U的所有邻居到起点的距离,即“松弛”,而prim则不需要。所以,只需要把Dijkstra的程序简化一些即可。
和Dijkstra算法一样,prim程序也可以用优先队列来查找距离U最近的点,能优化算法。
prim的编程比较麻烦,kruskal更简单且高效。

2、kruskal算法
kruskal算法编程有以下两个关键技术:
(1)对边进行排序。可以用STL的sort()函数,排序后,依次把最短的边加入到T中。
(2)判断圈,即处理连通性问题。这个问题用并查集简单而高效,并查集是kruskal算法的绝配。
任意上面的图为例说明kruskal算法的操作步骤

在这里插入图片描述

(1)初始时最小生成树为空。令S是以节点i为元素的并查集,在开始时,每个点属于独立的集(为了便于讲解,下表中区分了结点i和集S,把集的编号加上可下划线):
在这里插入图片描述
(2)加入第一个短边(1-2):T={1-2}。在并查集S中,把结点2合并到结点1,也就是把结点2的集2改成结点1的集1。
在这里插入图片描述
(3)加入第二个短边(3-4),T={1-2,3-4}。在并查集S中,结点4合并到结点3.
在这里插入图片描述
(4)加入第三个最短边(2-5):T={1-2,3-4,2-5}。在并查集S中,把结点5合并到结点2,也就是把结点5的集5改成结点2的集1.在集1中,所有结点都指向了根结点,这样可以避免并查集的长链问题。
在这里插入图片描述
(5)第四个最短边(1-5)。检查并查集S,发现5已经属于集1,丢弃这个边。这一步实际上是发现了一个圈。并查集的作用就体现在这里。
(6)加入第五个最短边(2-4)。在并查集S中,把结点4的集并到结点2的集。注意这里结点4的集原本为3,实际上的修改是把结点3的集3改成1。
(7)重复以上操作,直到结束。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int NUM=105;
int S[NUM];//并查集
struct Edge{int u,v,w;}edge[NUM*NUM];
bool cmp(Edge a,Edge b){return a.w<b.w;};
int find(int u){return S[u]==u?u:find(S[u]);}
int n,m;
int kruskal(){
    int ans=0;
    for(int i=1;i<=n;++i)S[i]=i;
    sort(edge+1,edge+m+1,cmp);
    for(int i=1;i<=m;++i){
        int b=find(edge[i].u);
        int c=find(edge[i].v);
        if(b==c)continue;//产生了圈,丢弃这个边
        S[c]=b;//合并
        ans+=edge[i].w;//计算MST
    }
    return ans;
}
int main(){
    while(~scanf("%d",&n)){
        if(!n)return 0;
        m=n*(n-1)/2;
        for(int i=1;i<=m;++i)
            scanf("%d%d%d",&edge[i].u,&edge[i].v,&edge[i].w);
        printf("%d\n",kruskal());
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

布布要成为最负责的男人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值