| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 数据结构与算法 -> 算法设计与分析课程学习-第四章 贪心算法 -> 正文阅读 |
|
[数据结构与算法]算法设计与分析课程学习-第四章 贪心算法 |
贪心算法的基本概念?
? ? ? ? 上一章在介绍01背包问题时就简单地提到了贪心算法.在普通的背包问题中,向容量有限的背包中装入多种物品,每件物品存在重量与价值两个属性,并允许被拆分,要求计算背包可以容纳的最大价值.用贪心的思路去想,显然我们要优先装入单位重量价值更大的物品.根据这个思路,就可以轻松地解决背包问题了. ? ? ? ? 贪心算法可以理解为在每一个局部选择局部的最优解,并得到一个整体的最优解.当然,在很多情况下,贪心算法得到的结果并不是真正的最优解,因为题目考察的可能是动态规划这种更具有考察价值的算法.但是,动态规划得到的结果往往会比较接近最优解.所以贪心算法在算法题中,可以作为动态规划题目找不到状态转移方程时的骗分手段,有那么多测试点,总得有贪心算法恰好是最优解的情况吧. ? ? ? ? 当然,不去谈这些邪门歪道,贪心算法也是具有很深远的意义的,它更多的是一种解决问题的思路.例如面对单源最短路径问题的重要算法Dijkstra算法,以及面对最小生成树问题的Prim算法与Kruskal算法,都采用了贪心的思路.今天我们试着理解贪心算法,也主要通过上述的三个算法来进行.需要声明的是,由于笔者精力和能力的限制,上面的算法我的描述也许会相对抽象,不会有太多生动的模拟过程,读者如果感到理解吃力,我会附上推荐的学习链接地址. Dijikstra算法解决单源最短路问题单源最短路问题描述:????????在一个图中,求某个点到其它所有点的最短路径的长度. ????????在此我们采用下面的图例来模拟算法过程.如下图所示是一个图,图中包括多条边,每条边有不同的权值,所谓最短路径就是从一个点走到另一个点走过的边的权值之和最小.我们需要求出结点1到其它所有点的最短路长度. ? ? ? ? 对于这张图,我们可以采用邻接矩阵的方式储存,在此我们设为Graph[][],其中Graph[i][j]代表结点i到结点j的边的权值.对两个并不直接相连的结点,它们之间的边权值被认为是无穷大.下面的表是对本案例我们初始的Graph[][]邻接矩阵.
?????????之所以认为贪心思路被应用到了Dijikstra算法中,是因为Dijikstra算法在每一步中,假如这一步中的optimal[x]为最短路径,那么我们就认为初始结点1到结点x的路径已经确定,这种取局部最优解的思路正是一种贪心思路. Dijikstra算法的过程模拟? ? ? ? 下面我们来模拟Dijikstra算法的过程: 第一步: ????????对于初始状态的optimun数组:
? ? ? ?除了1结点自身对自身的距离为0,,我们发现当前的最短路径是1->2的路径,距离为1,那么我们认为1->2的最短路径已经确定就是1,我们将结点2加入已经确定的图中. ???????我们可以理解为当前只考虑了下面的局部图,其中绿色的节点代表最短路已经确定的集合: ? ? ? ? 第二步:? ????????既然节点2已经确定,不妨将节点2的直接相邻节点也加入optimum[]数组,如果1->2已经是最短的了,那么1->2的临点也会有可能最短的通路.首先,2->3,且Graph[2][3]+optimum[2]=1+9=10<optimum[3]=12,其次,2->4,且Graph[2][4]+optimum[2]=1+3=4<optimum[4]=MAX_INT,这两个点通过新加入的节点2取得了更短的路径,所以我们要修改optimum[]的相关数值:
? ? ? ? ?除了已经确定的节点1,2,我们发现节点4具有optimum[4]=4是当前最短的路径,我们认为节点4的最短路径已经确定. ???????? ? ? ? ? ?根据上面的算法过程,我们继续模拟,详细解析不再赘述. 第三步: ????????
第四步:
?第五步:
????????至此,每个节点都已经加入我们的确定集合,也就是说,optimum[]数组中的每一个元素都已经是最短路径的数值了.这就是Dijikstra算法的过程. Dijikstra算法的编码实现Dijikstra算法的实现如下:
最小生成树问题最小生成树问题描述????????在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。我们的目标就是找到一个连通图中的最小生成树。 ? ? ? ? 例如对下图: ? ? ? ? 存在最小生成树: ???????? ? ? ? ? 面对这个问题,经典的算法有两种:Prim算法与Kruskal算法。接下来我们会分别讨论两种算法的实现和它们的联系。 Kruskal算法Kruskal算法过程模拟:? ? ? ? Kruskal算法可以理解为加边法。在初始状态下,我们认为所有节点都是孤立存在的,然后将边由短到长加入到图中,直到这个图成为一个连通图。这也是一个典型的贪心思路:我希望路径长度总和最短,那么在每一步,我都希望取到尽量短的路径。 ? ? ? ? 值得注意的是,除了要保证新加入的边是最短的,也要保证新加入的边可以将两个连通分量连接,如果这条边的加入不能减少连通分量的个数,那么它是没有意义的。 ????????下面我们来模拟这个过程: 初始状态: ? ? ? ? 此时图中的点都是孤立存在的。接下来我们要做的就是在图中加边: 第一步:? ? ? ? ? 显然在原图中,最短的边是A-C的边,长度为1,同时,它的加入也连接了A与C两个连通分量。 ? ? ? ? 在添加这条边后,显然这张图仍然不是连通图。所以我们需要继续算法。 第二步: ? ? ? ? 在图中尚未使用的边中,最短的是D-F边,长度为2。它显然也连接了两个连通分量。 ? ? ? ? 在添加这条边后,仍然不是连通图,所以我们需要继续算法。 ???????? 第三步: ????????在图中尚未使用的边中,最短的是B-E边,长度为3。它显然也连接了两个连通分量。 ? ? ? ? 在添加这条边后,仍然不是连通图,所以我们需要继续算法。 第四步: ????????在图中尚未使用的边中,最短的是C-F边,长度为4。它显然也连接了两个连通分量。 ? ? ? ? 在添加这条边后,仍然不是连通图,所以我们需要继续算法。 第五步: ? ? ? ? 在图中尚未使用的边中,最短的是B-C边与C-D边,它们的长度都为5,此时我们需要选择可以减少连通分量个数的边。 ? ? ? ? 假如我选择添加C-D边: ? ? ? ? 在添加边之前和之后,连通分量都是两个,所以这条边没有意义,反而增加了路径的开销。此外,图中出现了环路,也不再是一棵树。 ? ? ? ? 而选择B-C边可以满足我们的要求: ?????????此时,整张图已经连通,算法结束,这个过程中我们生成的图就是满足要求的最小生成树。 Kruskal算法编码实现:? ? ? ? Kruskal算法并不是那种理解了原理就可以轻松编码的算法.在编码中,我们面临多个难点. 1.如何存储一张图? ? ? ? ? 最简单的方式方式,就是通过一个邻接矩阵来存储.然而,对于本题目而言,我们需要选择最短的边,然后是次短的边...如果采用邻接矩阵,在每次选择边的时候,都要将二维数组遍历,这是一个难以接受的时间复杂度.所以我们采用了一种以边为核心的存储方式:通过一个一维数组存储所有的边.一条边可以由两个端点和它的长度组成,所以这个数据结构的实现如下:
? ? ? ? 这样的话,我们只需要对保存边的e[]数组根据value值进行排序,就可以轻松地按长度依次取出图中的边了. 2.如何描述图的连通度这个特征?怎么确定新加入的边是否减少了图的连通分量数量? ? ? ? ? 这里引入一个用来解决图的连通问题的非常重要的子算法:并查集算法.并查集是专门用来维护图的连通性,或者判断图中是否有环的重要算法.顾名思义,并查集就是对集合的合并和查询操作.关于并查集的内容完全可以作为一个独立的课题,这里我们就题论题.实现对节点所在集合的维护即可. ? ? ? ? 使用并查集,就要将每个联通分量看作一棵树,判断两个节点是否在同一个连通分量中,本质上就是判断两个节点所在的树的根节点是否相同.下面给出实现这一过程的代码: ? ? ? ? 首先,通过par[]数组存储每个节点的父节点,如果本身是根节点,父节点就是本身.
? ? ? ?对于本题目而言,在初始状态下,每个节点都是一棵树,且都是父节点.随着边加入图中,树需要进行合并.下面的代码实现了这一过程.
? ? ? ? 下面给出本题目整体的代码:
? ? ? ? 最后输出在最小生成树中保留的边的属性. Prim算法Prim算法过程模拟? ? ? ? Prim算法可以理解为加点法.在初始状态下,我们认为图中只有一个节点,然后根据当前图中最短的边,添加与最短边相连的节点,直到原图中所有的点全部相互连接,就得到了我们的最小生成树. ? ? ? ? 与Kruskal算法相对应的,在选择最短边时,要保证最短边连接的节点尚未被连接,否则这条边将是没有意义的. ? ? ? ? 下面我们来模拟这个过程: ? ? ? ? 仍然使用这张图作为例子: 初始状态: ? ? ? ? 我们以A点作为初始节点,开始这个算法.当然,使用任意节点都是可以得到最小生成树的. ????????? ?第一步: ? ? ? ? 观察当前的图的情况,发现最短的边是权值为1的这一条,同时这条边所连接的节点必然还没有加入当前的图中,因此我们将长度为1的边连接的节点加入图中. ???????? 第二步: ? ? ? ? 此时我们观察图中所有的边,发现最短的是长度为4的这一条.且其连接的节点尚未加入图中,因此我们将新节点加入图中. 第三步: ? ? ? ? ?同理,将长度为2的边连接的点加入图中. 第四步: ? ? ? ? ? 第五步: ? ? ? ? ?此时,所有节点都已经加入了图中.标记出已经使用过的边,发现这便是一棵最小生成树. ? ? ? ? 另外,图中除了红色的边,其它边都不是真实存在的.只是在添加新节点时,我们需要考虑这些边,画出它们是为了便于观察. Prim算法的编码实现? ? ? ? 唯一需要注意的是,根据题目的需求,我们这次存储图使用了点和边结构体相嵌套.事实上这钟写法的复杂度已经与邻接矩阵区别不大了.在面对图论的题目时,图的存储结构是一个需要谨慎的点,好的数据结构可以尽可能降低时间复杂度. ????????与Kruskal算法相比,Prim算法没有太多实现上的困难,在此不再赘述.直接展示笔者的实现代码. ? ? ? ? 笔者没有进行精心的优化,可以看到,在暴力查找最短节点的时候,算法的时间复杂度可以达到O(n^3),欢迎笔者提出优化方案.
总结? ? ? ? 我们把边数远远小于相同节点组成的完全图的图称为稀疏图.把边数接近完全图的图称为稠密图.由于Kruskal算法是通过加边得到的最小生成树,在边过多时必然会降低效率,所以Kruskal算法更适合稀疏图求最小生成树.相比较而言,Prim算法每次寻找的是节点,所以即使在图非常稠密的情况下,这种思路可以忽略很多没必要遍历的节点.因此Prim算法更适合稠密图求最小生成树.这是在应用时,两者的最主要区别. |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/26 10:14:43- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |