当前位置: 首页 > news >正文

最小生成树算法的解题思路与 C++ 算法应用

一、最小生成树算法针对问题类型及概述

先来简要陈述一下树的概念:一个由 N N N 个点和 N − 1 N-1 N1 条边组成的无向连通图。由此,我们可以得知生成树算法的概念:在一个 N N N 个点的图中找出一个由 N − 1 N-1 N1 条边组成的树。

具体来说,我们是在一个图 G ( N , M ) G(N,M) G(N,M) 中找到一个生成树 G ( N , N − 1 ) G(N,N-1) G(N,N1) ,在生成树 G ( N , N − 1 ) G(N,N-1) G(N,N1) 中的所有边 ∀ w ∈ G ( N , N − 1 ) \forall w \in G(N,N-1) wG(N,N1) 中,我们要不断调整我们找到的生成树,使得所有边权之和 ∑ x ∈ G ( N , N − 1 ) x \sum_{x \in G(N,N-1)} x xG(N,N1)x 最小。因此,我们得出了最小生成树的概念。

最小生成树算法一般有两种较为常用的算法,分别是 Prim 算法和 Kruscal 算法。 Prim 算法和最短路中的 Dijkstra1 算法十分相似,以为对象进行搜索,时间复杂度为 O ( n 2 ) O(n^2) O(n2) 。 Kruscal 算法以为对象进行搜索,先将所有边进行排序,再一次抽取所需要的边组成最小生成树,时间复杂度约为 O ( m log ⁡ m ) O(m\log m) O(mlogm) 。由前面学习最短路算法的经验可知,由于 Prim 算法注重点的数量,时间效率与点的数量大致成正比,所以 Prim 算法适用于稠密图(点多边少);由于 Kruscal 算法注重边的数量,时间效率与边的数量大致成正比,所以 Kruscal 算法适用于稀疏图。

最小生成树一般可以用来解决在一个图中找出一个既包含所有点,又最小的结构问题;结合实际应用时,最小生成树算法可以有更多的用途。

二、最小生成树算法的过程

下面我们来介绍引言中提到的两种算法,Prim 算法和 Kruscal 算法。

1. Prim 算法的详细过程及主要思想

Prim 算法主要设置一个状态 d ( i ) d(i) d(i) 表示第 i i i 个点到最小生成树的距离。

为了更加具象化地展现 Prim 最小生成树算法,下面我将举如下的一个例子来说明该算法的原理。假设我们现在有一张图 G ( M , N ) G(M,N) G(M,N)

3
1
2
10
1
20
1
d = 0
2
d = inf
3
d = inf
4
d = inf

首先,我们会像 Dijkstra 算法一样,将 d d d 数组初始化为 ∞ \infin ,然后将 d ( 1 ) = 0 d(1)=0 d(1)=0 。注意,由于这里是最小生成树算法,所以使任一点作为计算的源点都是合理的操作。

接下来,我们围绕源点 1 1 1 更新其周边的点 i i i d ( i ) d(i) d(i) 值为 1 , i → \overrightarrow{1,i} 1,i

3
1
2
10
1
20
1
d = 0
2
d = 3
3
d = 1
4
d = 2

然后,我们捡取其中 d d d 值非零且最小的点,得到 3 3 3 号节点,因此将 3 3 3 号节点归入最小生成树中,并将 d ( 3 ) = 0 d(3)=0 d(3)=0 ,随后更新与 3 3 3 号节点相邻的所有节点的 d d d 值。注意,并不是所有节点都一定需要更新,而是与之前取得与最小生成树的距离的值去较大值。在这个实例中,我们需要更新 2 2 2 号节点。此时,最小生成树为 1 − 3 1-3 13

3
1
2
10
1
20
1
d = 0
2
d = 3
3
d = 0
4
d = 1

重复上面的步骤,选取此时的最小值 4 4 4 号点,相应地更新其周围的点。此时,最小生成树为 1 − 3 − 4 1-3-4 134

3
1
2
10
1
20
1
d = 0
2
d = 3
3
d = 0
4
d = 0

因此,我们的最小生成树为 1 − 3 − 4 − 2 1-3-4-2 1342 ,总代价为 1 + 2 + 1 = 4 1+2+1=4 1+2+1=4

更加抽象、准确的表述为: Prim 算法是一种贪心算法,先设置一个状态 d d d 表示节点与最小生成树的距离。再选定一个源点,更新该点和它周围的点的 d d d 值,将现在的 d d d 值作为初始值。在程序设计的时候,可以认为开始的时候所有点离最小生成树的距离均为 ∞ \infin ,然后通过第一次更新将他们重新赋值。赋值操作的状态转移方程如下:
d i = min ⁡ j → i { i , j ‾ } d_i=\min_{j \rightarrow i}\{\overline{i,j}\} di=jimin{i,j}
此处, j j j 为当前所在的点, i i i 为要更新的点, j → i j \rightarrow i ji 表示能从 j j j 出发到达 i i i ,即 i , j i,j i,j 之间连通。接下来,我们选择一个当前 d d d 值不为零且最小的点,继续重复上面的步骤,直到所有点的 d d d 值均为 0 0 0 ,算法结束。

统计答案时,设置一个变量 A A A ,当程序遍历到一个节点的时候,就在当前节点的 d d d 值被 0 0 0 覆盖之前把 d d d 值累加到 A A A 里面去,表示一个边权,最后的最小代价即为 A A A

Prim 算法的代码实现如下:

#include <bits/stdc++.h>using namespace std;const int N = 107;struct Node
{int to, wi;
};int n, m, d[N], v[N], ans;
vector<Node> e[N];
vector<int> q;int main()
{cin >> n >> m;for (int i = 1, a, b, c; i <= m; ++i){cin >> a >> b >> c;e[a].push_back({b, c});e[b].push_back({a, c});}memset(d, 0x3f, sizeof(d));d[1] = 0;for (int i = 1; i <= n; ++i){int minn = 1e9, p = 0;for (int j = 1; j <= n; ++j){if (d[j] < minn && !v[j]){minn = d[j];p = j;}}v[p] = 1;ans += d[p];for (Node j : e[p]){if (!v[j.to]){d[j.to] = min(d[j.to], j.wi);}}}cout << ans << endl;return 0;
}

2. Kruscal 算法的主要思想及实现

Kruscal 算法与 Prim 算法不同,使用边为对象进行搜索。Kruscal 通过先把每条边分别存储并进行排序,计算出每次代价较小的边。判断如果这条边的两个端点都不在树之内,则我们将这条边选入最小生成树。如果这条边的两个端点都在树之内,则说明不能加上这条边。证明如下:
如果我们在一棵树中再添加一条边,这个树将满足 G ( N , N − 1 ) → G ( N , N ) G(N,N-1) \rightarrow G(N,N) G(N,N1)G(N,N) ,从而与树的定义相违背。因此,我们不能再一棵树中再次添加一条边。
在判断每个节点是否属于一棵树的时候,我们应该使用并查集2,即每当加入一条新边的时候,将新加入的两个节点合并为一个集合。如果两个节点属于不同的集合,则合并两个集合,方法与普通并查集的方法相同,无方向、无边权。最后,我们只需统计每次选入并查集的边的边权之和。

具体操作如下:如果两个节点 i , j i,j i,j 的父节点不属于同一个集合,则将这条边 i , j ‾ \overline{i,j} i,j 加入最小生成树。反之,跳转到下一条边。

一个实例如下:
我们定义一张图如下:

3
1
2
10
3
6
1
2
3
4

现将所有边排序,得到边权的顺序为 { 1 , 2 , 3 , 3 , 6 , 10 } \{1,2,3,3,6,10\} {1,2,3,3,6,10}

接下来,我们按照 Kruscal 算法的顺序进行操作:

  • 选择第一条边 2 , 4 ‾ \overline{2,4} 2,4 ,边权为 1 1 1 ,加入最小生成树:
3
1
2
10
3
6
1
2
fa = 2
3
4
fa = 2
Cost
ans = 1
  • 选择第二条边 3 , 4 ‾ \overline{3,4} 3,4 ,边权为 2 2 2 ,加入最小生成树:
3
1
2
10
3
6
1
2
fa = 2
3
fa = 2
4
fa = 2
Cost
ans = 3
  • 选择第三条边 2 , 3 ‾ \overline{2,3} 2,3 ,但由于 f a ( 2 ) = f a ( 3 ) = 2 \mathrm{fa}(2)=\mathrm{fa}(3)=2 fa(2)=fa(3)=2 ,所以忽略这条边。

  • 选择第四条边 1 , 2 ‾ \overline{1,2} 1,2 ,边权为 3 3 3 ,加入最小生成树。

3
1
2
10
3
6
1
fa = 1
2
fa = 1
3
fa = 1
4
fa = 1
Cost
ans = 6

至此,我们已经完成了最小生成树的构建,结构为 1 − 2 − 4 − 3 1-2-4-3 1243 ,边权总和为 3 + 1 + 2 = 6 3+1+2=6 3+1+2=6

代码实现时,注意并查集的加入即可,具体实现方法如下。

#include <bits/stdc++.h>using namespace std;const int N = 307;struct Node
{int u, v, w;
} e[N * N];int n, m, fa[N], v[N], ans;int getfa(int x)
{if (fa[x] == x) return x;return fa[x] = getfa(fa[x]);
}int main()
{cin >> n >> m;for (int i = 1; i <= n; ++i){fa[i] = i;}for (int i = 1; i <= m; ++i){cin >> e[i].u >> e[i].v >> e[i].w;}sort(e + 1, e + m + 1, [&](Node a, Node b){return a.w < b.w;});int sel = 0;for (int i = 1; i <= m && sel < n - 1; ++i){int fx = getfa(e[i].u), fy = getfa(e[i].v);if (fx != fy){fa[fx] = fy;ans = max(ans, e[i].w);sel++;}}cout << n - 1 << " " << ans << endl;return 0;
} 

三、最小生成树算法的实际应用

1. 使用最小生成树算法实现在一个图中选择若干条边将整个图连通且代价最小

这其实本质上就是最小生成树算法。因为树是一个最小的连通图。证明如下:

用反证法。假设我们有一个图 G ( N , N − 2 ) G(N,N-2) G(N,N2) 为连通图,则我们可以知道在这个图中,任意两点间都有路径相连。因为这张图一共有 N − 2 N-2 N2 条边,由鸽巢原理,知最多可以放下 N − 1 N-1 N1 个节点,与条件中的 N N N 个节点相矛盾。由数学归纳法,知对于所有 1 ≤ M ≤ N − 2 1 \le M \le N-2 1MN2 G ( N , M ) G(N,M) G(N,M) 均不可能是连通图。

从这个推论,我们可以解决大部分的最小生成树问题,剖析题目中所给出的已知条件,一旦有形如“在一个图中选择若干条边将整个图连通且代价最小”的字眼,就一定是最小生成树算法。


  1. Dijkstra 算法的详细用途详见这篇文章。文章链接 ↩︎

  2. 并查集算法的详细用途详见这篇文章。文章链接 ↩︎

相关文章:

  • aws各类服务器编号
  • AWS RDS :多引擎托管数据库服务
  • RK3568笔记八十三:RTMP推流H264和PCM
  • VINS-Mono论文阅读笔记
  • 【Python3教程】Python3基础篇之命名空间和作用域
  • 安科瑞ASJ系列漏电流继电器:守护地铁配电安全的利器
  • ZArchiver:高效解压缩,轻松管理文件
  • 系统的性能优化
  • 管件接头的无序抓取
  • 如何用K8s+Istio进行云原生开发?
  • 固态硬盘的加装和初始化
  • Uniapp启动页白屏问题深度解析与全面解决方案
  • Flutter Melos在外包团队协作中的弊端与应对策略
  • JSX 详解:React 的核心语法
  • 用idea操作git缓存区回退、本地库回退、远程库回退
  • python爬虫关于多进程,多线程,协程的使用
  • 20.jsBridge多页面交互与原生事件监听冲突问题
  • 04、eigen库实现插值算法与matlab对比
  • C#核心学习
  • 构建智能问答系统:从零开始实现 RAG 应用
  • 广州建外贸网站/网站为什么要做seo
  • 写作网站挣钱对比/全球最大的中文搜索引擎
  • 可以随意做配搭的网站/市场推广是做什么的
  • 六十岁一级a做爰片免费网站/成品短视频软件大全下载手机版
  • 网站开发市场现在怎么样/国内十大搜索引擎
  • 济南网站建设培训学校/网络推广渠道有哪些