在学习了图的基本结构和遍历方式后,我们再继续地深入学习一些图的基本应用。在之前的数据结构中,我们并没接触太多的应用场景,但是图的这两类应用确是面试或考试中经常出现的问题,而且出现的频率还非常高,不得不来好好说一说。

什么是最小生成树?

从前面的学习中,我们应该能够发现,图就是一种扩展的树结构。对于树来说,它只有一个上级结点,同级结点之间没有关联。而图则打破了树的这些规则。我们再反过来想想,能不能给定一个条件,那就是连接上所有的结点,但是每个结点之间只保留一条边。这样形成的一颗简单的树其实就是能够串联所有结点的一条路径,而最小生成树的概念,其实就是对于有权图来说,权数最少的那条能够串连起所有结点的边的路径,或者也可以说是最小连通树、最小连通子图、最小代价树。

从上图中就可以看出,对于一个有权图来,可以有许多生成树的方式,不过不同的路线方式的结果会不同,只有最后一个路径形成的生成树具有路径最小的那颗树,就是我们需要的最小生成树。

为什么要强调是有权图呢?因为如果是无权图,所有结点连接起来的方案其实就没有什么太大的意义了,因为不管从哪个结点出发走哪条路径可能权值都是一样的。而带权路径则总会有一条最佳的路径是可以将所有结点遍历完成并且权数还是最小的。最典型的应用就是地图上哪条线路成本最少呀,办公楼布线怎么走线最经济之类相关的题目,基本都会牵涉到最小生成树的概念。

关于最小生成树的最经典的算法,Prim 和 Kruskal 这两个大神级别的算法是绕不过去的槛,下面我们就来粗浅地学习一下。

第一种算法 Prim

Prim 算法,中文名 普里姆 算法。起源就不多说了,反正是个人名,这篇文章和下篇文章中图的应用的这些算法名称都是人名相关的。他们发现并最初使用了这些算法,然后就将这些算法以他们的名字命名了。

Prim 算法的核心思想就是:从一个结点出发,查看这个结点的所有的边中权值最小的那条边,然后加上这条边所连接的那个结点的所有边,再一起看哪个边的权值最小,然后一直重复这些步骤,反正就是所有结点到我们出发的这个结点中所有权值最小的边都看一遍,并且让它们能够连接所有结点就完成了。

看图是不是就清晰多了。我们一步一步地看。

    1. 首先我们从第 1 个结点出发,然后看第 1 个结点相关的边哪个权值最小,很明显,我们要选选择 <1, 2> 这条边,然后将结点 2 加入到选择中
  • 2)在结点 1 和结点 2 中选择最权值最小的边并连接到新的结点,在这里我们选择的是 <1, 3> 这条边,于是结点 3 也加入到选择中

  • 4)在结点 1、2、3 的所有边中,选择权值最小的边,可以看到 <2, 3> 这条边的权值最小,但是 2 和 3 都已经连通了,所以选择下一个最小的边 ️, 4> ,结点 4 还没有加入到已经连通的结点中,于是就走 ️, 4> 这条边,结点 4 加入已连通结点中

  • 5)同样地,在结点 1、2、3、4 中选择权值最小的边,这时只有 <4, 6> 边是最小的,并且结点 6 也没有加入到已连通结点中,选择这条路线,结点 6 加入连通结点中

  • 6)最后,在结点 1、2、3、4、6 中查找权值最小的边,得到 <6, 5> 这条边,结点 5 也没连通,于是选择这条路径,加入结点 5

  • 7)所有结点都已经连通,权值累加结点为 19 ,当前的这条路径就是最小权值路径,所形成的这一条路径就是一颗最小生成树了

从这个步骤和图释来说,大家可以自己尝试写写这个 Prim 算法的代码,其实并不复杂。我们需要一个集合来放置已经连通的结点信息,当查找路径的时候找到的最小权值路径连通的结点不在集合中,就加入到集合中。然后不断累加所有的路径权值,最后就得到了遍历整张图的最小生成树路径。

// 普里姆算法
function Prim($graphArr)
{
$n = count($graphArr);
// 记录 1 号顶点到各个顶点的初始距离
$dis = [];
for ($i = 1; $i <= $n; $i++) {
$dis[$i] = $graphArr[1][$i];
} // 将 1 号顶点加入生成树
$book[1] = 1; // 标记一个顶点是否已经加入到生成树
$count = 1; // 记录生成树中的顶点的个数
$sum = 0; // 存储路径之和
// 循环条件 生成树中的顶点的个数 小于 总结点数
while ($count < $n) {
$min = INFINITY;
for ($i = 1; $i <= $n; $i++) {
// 如果当前顶点没有加入到生成树,并且记录中的权重比当前权重小
if (!$book[$i] && $dis[$i] < $min) {
// 将 $min 定义为当前权重的值
$min = $dis[$i];
$j = $i; // 用于准备将顶点加入到生成树记录中
}
}
$book[$j] = 1; // 确认将最小权重加入到生成树记录中
$count++; // 顶点个数增加
$sum += $dis[$j]; // 累加路径和
// 调整当前顶点 $j 的所有边,再以 $j 为中间点,更新生成树到每一个非树顶点的距离
for ($k = 1; $k <= $n; $k++) {
// 如果当前顶点没有加入到生成树,并且记录中的 $k 权重顶点大于 $j 顶点到 $k 顶点的权重
if (!$book[$k] && $dis[$k] > $graphArr[$j][$k]) {
// 将记录中的 $k 顶点的权重值改为 $j 顶点到 $k 顶点的值
$dis[$k] = $graphArr[$j][$k];
}
}
}
return $sum;
} $graphArr = [];
BuildGraph($graphArr); // 之前文章中的生成邻接矩阵的函数 echo Prim($graphArr); // 19

我们运行代码并输入测试数据。

php 5.4图的应用:最小生成树.php
请输入结点数:6
请输入边数:9
请输入边,格式为 出 入 权:2 4 11
请输入边,格式为 出 入 权:3 5 13
请输入边,格式为 出 入 权:4 6 3
请输入边,格式为 出 入 权:5 6 4
请输入边,格式为 出 入 权:2 3 6
请输入边,格式为 出 入 权:4 5 7
请输入边,格式为 出 入 权:1 2 1
请输入边,格式为 出 入 权:3 4 9
请输入边,格式为 出 入 权:1 3 2
19

可以看到输出的结果和我们预期的一样。代码中已经有很详细的注释说明了,如果直接看代码比较晕的话,大家可以拿调试工具进行断点的单步调试来看一下具体的运行情况。在这里我们先看一下那个 dis[] 中最后都保存了什么东西。

Array
(
[1] => 9999999
[2] => 1
[3] => 2
[4] => 9
[5] => 4
[6] => 3
)

INFINITY 是我们定义的一个常量,在初始化 graphArr 这个邻接矩阵时,将所有的边都设置为 INFINITY 了,主要就是方便我们后面进行最小值的比对。这个 INFINITY 我们设置的是 9999999 这样一个非常大的数。dis[] 中其实包含的就是结点 1 所经过的每条边所选择的权值,把他们加起来就是我们的最终路径长度。

第二种算法 Kruskal

Prim 算法好玩吗?相信通过具体的算法你对最小生成树的概念就更清晰了,不知道你会不会有个这样的想法:直接遍历所有的边,给他们按权值排序,这样我们再依次遍历这个排序后的边结构数组,然后将边的结点加入到最终要生成的树中,这样不也能形成一个最小生成树嘛!哇塞,你要是真的想到这个方案了那要给一个大大地赞了。这种方式就是我们最小生成树的另一种明星算法:Kruskal 算法。它的中文名字可以叫做 克鲁斯卡尔 算法。

看这个步骤是不是和 Prim 就完全不一样了?不急,我们还是一步一步地来看。

  • 1)在所有的边中,选择最小的那条边,也就是 <1, 2> 这条边,结点 1 和结点 2 连通

  • 2)接着选择第二小的边,<1, 3> 边符合条件,并且结点 3 没有连通,加入结点 3

  • 3)继续选择最小的边,此时最小的边是 <4, 6> ,这两个结点都没有连通,直接加入

  • 5)接下来是 <6, 5> 这条边最小,继续连通并将结点 5 加入

  • 6)好了,左右两边成型了,现在最小的边是 <2, 3> 边,不过结点 2 和结点 3 已经连通了,放弃!选择 <4, 5> 边,同样,结点4 和结点 5 也已经连通了,放弃!选择 ️, 4> 边,OK,这两条边还没有连通,直接连通,所有结点连通完毕,最小生成树完成!

不错吧,又学会一个新的套路,大家也可以试试按照上面的步骤和图释来自己先写写代码。需要注意的我们要先给所有的边排序,才能进行这个算法的操作。另外,每次判断结点连通也是一件费事的工作,使用深度优先或者广度优先遍历是没问题的,但效率太低,让我们看看大神(算法书中)们是怎么做的。

// 克鲁斯卡尔算法
function Kruskal($graphArr)
{
global $map, $f;
$hasMap = [];
$i = 1;
// 转换为序列形式方便排序
// O(mn)或O(n^2),可以直接建图的时候使用单向图进行建立就不需要这一步了
foreach ($graphArr as $x => $v) {
foreach ($v as $y => $vv) {
if ($vv == INFINITY) {
continue;
}
if (!isset($hasMap[$x][$y]) && !isset($hasMap[$y][$x])) {
$map[$i] = [
'x' => $x,
'y' => $y,
'w' => $vv,
];
$hasMap[$x][$y] = 1;
$hasMap[$y][$x] = 1;
$i++;
}
}
}
// 使用快排按照权重排序
quicksort(1, count($map)); // 初始化并查集
for ($i = 1; $i <= count($graphArr); $i++) {
$f[$i] = $i;
} $count = 0; // 已记录结点数量
$sum = 0; // 存储路径之和
for ($i = 1; $i <= count($map); $i++) {
// 判断一条边的两个顶点是否已经连通,即判断是否已在同一个集合中
if (merge($map[$i]['x'], $map[$i]['y'])) { // 如果目前已连通,则选用这条边
$count++;
$sum += $map[$i]['w'];
}
if ($count == count($map) - 1) { // 直到选了n-1条边后退出
break;
}
}
return $sum;
}

Oh my God!代码多了好多,还有好多莫名其妙的东西出现了。在上文中说过,我们要使用 Kruskal 算法就得先给边排序。所以我们先将邻接矩阵转换成 map[x,y,w] 的形式,x 和 y 依然是代码两个结点,而 w 代表权重。这样的一个可以看成是边对象的数组就比较方便我们进行排序了。

接着我们使用快速排序按照权值进行排序,具体的快排算法我们在后面学习排序的时候再详细说明,大家可以直接在文章底部复制测试代码链接查看完整的代码。

接下来就是使用并查集进行 Kruskal 算法的操作了。并查集就是代替深度和广度优先遍历来快速确定结点连通情况的一套算法。

$f = [];

// 并查集寻找祖先的函数
function getf($v)
{
global $f;
if ($f[$v] == $v) {
return $v;
} else {
// 路径压缩
$f[$v] = getf($f[$v]);
return $f[$v];
}
} // 并查集合并两子集合的函数
function merge($v, $u)
{
global $f;
$t1 = getf($v);
$t2 = getf($u);
// 判断两个点是否在同一个集合中
if ($t1 != $t2) {
$f[$t2] = $t1;
return true;
}
return false;
}

它本身还是通过递归的方式来将结点保存在一个数组中,通过判断两个点是否在同一个集合中,即两个结点是否有共同的祖先来确定结点是否已经加入并且连通。

关于并查集的知识本人掌握的也并不是很深入,所以这里就不班门弄斧了,大家可以自己查阅相关的资料或者深入研究各类算法书籍中的解释。

最后运行代码输出的结果和 Prim 算法的结果是一致的,都是 19 。

总结

怎么样?最小生成树是不是很好玩的东西,图的结构其实是很复杂的,不过越是复杂的东西能够玩出的花活也越多。但是反过来说,很多公司的面试过程中关于图的算法能考到这里的也都是大厂了,一般的小公司其实能简单地说一说深度和广度就已经不错了。我们的学习还要继续,下一篇我们将学习的是另一个图的广泛应用:最短距离。

今天的测试代码均根据 《啊哈!算法》 改写为 PHP 形式,参考资料依然是其它各类教材。

测试代码:

https://github.com/zhangyue0503/Data-structure-and-algorithm/blob/master/5.图/source/5.4图的应用:最小生成树.php

参考文档:

《数据结构》第二版,严蔚敏

《数据结构》第二版,陈越

《数据结构高分笔记》2020版,天勤考研

《啊哈!算法》

===============

关注公众号:【硬核项目经理】获取最新文章

添加微信/QQ好友:【xiaoyuezigonggong/149844827】免费得PHP、项目管理学习资料

知乎、公众号、抖音、头条搜索【硬核项目经理】

B站ID:482780532

【PHP数据结构】图的应用:最小生成树的更多相关文章

  1. <数据结构>图的最小生成树

    目录 最小生成树问题 Prim算法:点贪心 基本思想:类Dijstra 伪代码 代码实现 复杂度分析:O(VlogV + E) kruskal算法:边贪心 基本思想: 充分利用MST性质 伪代码 代码 ...

  2. K:图相关的最小生成树(MST)

    相关介绍:  根据树的特性可知,连通图的生成树是图的极小连通子图,它包含图中的全部顶点,但只有构成一棵树的边:生成树又是图的极大无回路子图,它的边集是关联图中的所有顶点而又没有形成回路的边.  一个有 ...

  3. 图结构练习——最小生成树(kruskal算法(克鲁斯卡尔))

    图结构练习——最小生成树 Time Limit: 1000ms   Memory limit: 65536K  有疑问?点这里^_^ 题目描述  有n个城市,其中有些城市之间可以修建公路,修建不同的公 ...

  4. 图结构练习——最小生成树(prim算法(普里姆))

      图结构练习——最小生成树 Time Limit: 1000ms   Memory limit: 65536K  有疑问?点这里^_^ 题目描述  有n个城市,其中有些城市之间可以修建公路,修建不同 ...

  5. 数据结构--图 的JAVA实现(上)

    1,摘要: 本系列文章主要学习如何使用JAVA语言以邻接表的方式实现了数据结构---图(Graph),这是第一篇文章,学习如何用JAVA来表示图的顶点.从数据的表示方法来说,有二种表示图的方式:一种是 ...

  6. 数据结构--图 的JAVA实现(下)

    在上一篇文章中记录了如何实现图的邻接表.本文借助上一篇文章实现的邻接表来表示一个有向无环图. 1,概述 图的实现与邻接表的实现最大的不同就是,图的实现需要定义一个数据结构来存储所有的顶点以及能够对图进 ...

  7. [从今天开始修炼数据结构]图的最小生成树 —— 最清楚易懂的Prim算法和kruskal算法讲解和实现

    接上文,研究了一下算法之后,发现大话数据结构的代码风格更适合与前文中邻接矩阵的定义相关联,所以硬着头皮把大话中的最小生成树用自己的话整理了一下,希望大家能够看懂. 一.最小生成树 1,问题 最小生成树 ...

  8. 数据结构-图-Java实现:有向图 图存储(邻接矩阵),最小生成树,广度深度遍历,图的连通性,最短路径1

    import java.util.ArrayList; import java.util.List; // 模块E public class AdjMatrixGraph<E> { pro ...

  9. java 数据结构 图

    以下内容主要来自大话数据结构之中,部分内容参考互联网中其他前辈的博客,主要是在自己理解的基础上进行记录. 图的定义 图是由顶点的有穷非空集合和顶点之间边的集合组成,通过表示为G(V,E),其中,G标示 ...

  10. java数据结构----图

    1.图:.在计算机程序设计中,图是最常用的数据结构之一.对于存储一般的数据问题,一般用不到图.但对于某些(特别是一些有趣的问题),图是必不可少的.图是一种与树有些相像的数据结构,从数学意义上来讲,树是 ...

随机推荐

  1. bat脚本中%~dp0含义解释

    在Windows脚本中,%i类似于shell脚本中的$i,%0表示脚本本身,%1表示脚本的第一个参数,以此类推到%9,在%和i之间可以有"修饰符"(完整列表可通过"for ...

  2. 一看就会的高效Discuz初始化入门安装方法

    在使用Discuz搭建论坛的过程中,小九发现有许多朋友对于宝塔的安装和初始化不太熟悉,找不到适合的方法.或是按照一些教程安装却出现问题得不到解决,只能选择重新再来. 今天,小九给大家介绍简单的镜像一键 ...

  3. 如何发送一个http请求—apipost

    API界面功能布局 API请求参数 Header 参数 你可以设置或者导入 Header 参数,cookie也在Header进行设置 Query 参数 Query 支持构造URL参数,同时支持 RES ...

  4. MATLAB—数组运算及数组化编程

    文章目录 前言 一.数组的结构和创建 1.数组及其结构 2.行数组的创建 3.对数组构造的操作 二.数组元素编址及寻访 1.数组元素的编址 2.二维数组元素的寻访 三.数组运算 非数的问题 前言 编程 ...

  5. NOIP 模拟 $33\; \rm Connect$

    题解 状压 \(\rm DP\). 从 \(1\) 到 \(n\) 一共只要一条路径,那么就是一条链,只要维护一个点集和当前链的末尾就行. 设 \(\rm dp_{i,j}\) 为 \(i\) 的点集 ...

  6. 动态规划_C#

    参考网址:https://blog.csdn.net/lvcoc/article/details/104167648 先不管动态规划,先看斐波那契数列 斐波那契数列:F1=Fn-1+Fn-2 分别用递 ...

  7. 分布式文件系统FastDFS搭建实操

    转载---------佳先森--- 一.什么是文件系统 分布式文件系统(Distributed File System)是指文件系统管理的物理存储资源不一定直接连接在本地节点上,而是通过计算机网络与节 ...

  8. 02.SpringMVC之初体验

    1.创建Maven WEB项目 2.导入springmvc的jar包 <dependencies> <dependency> <groupId>org.spring ...

  9. Ant高级-path和fileset

    一 <path/> 和 <classpath/> 你可以用":"和";"作为分隔符,指定类似PATH和CLASSPATH的引用.Ant会 ...

  10. linux 常用命令(三)——(centos7)MySql 5.7添加用户、删除用户与授权

    一.创建用户:以root用户登录到数据库进行用户创建 命令: CREATE USER 'username'@'host' IDENTIFIED BY 'password'; 例如: CREATE US ...