贪心法

算法导论

这个文档是学习“算法设计与分析”课程时做的笔记,文档中包含的内容包括课堂上的一些比较重要的知识、例题以及课后作业的题解。主要的参考资料是 Introduction to algorithms-3rd(Thomas H.)(对应的中文版《算法导论第三版》),除了这本书,还有的参考资料就是 Algorithms design techniques and analysis (M.H. Alsuwaiyel)。

贪心法

与动态规划一样,贪心法通常用于解决最优解的问题。但是与动态规划不同的是,贪心法通常由试图找到局部最优解的迭代过程组成。

贪心法与动态规划的另一个区别在于,贪心法是迭代的,而动态规划是递归的。

贪心法得到的解并不总是全局的最优解。

贪心法得到的解决方案通常是一步一步的(step by step),每一步都是在局部范围内在很少计算的基础上作出正确的猜测,而不用担心未来。

能够使用贪心法解决的问题通常具备两种性质:

  1. 最优子结构
  2. 贪心选择性

其中最优子结构性质和动态规划问题中的最优子结构性质是一样的。

而贪心选择性则是指一个问题的全局最优解可以通过局部最优解得到,或者说通过贪心法得到的解决方案是正确的,而这部分的证明通常是设计贪心算法最难的部分。

下面介绍一个简单的例子,背包问题。

假设现在有 n 件物品,每件物品的大小为 \(s_1,...,s_n\) 每个物品的价值为 \(v_1, ..., v_n\),现在有一个容量为 C 的背包来装这些物品,如何选择使得在背包容量的限制下装取的物品的价值总和最大?

这个问题使用贪心法就很好解决,先计算物品的性价比,\(r_i = v_i / s_i\),然后将所有物品按照性价比由高到低排序,然后按照这个排序进行选择,直到背包容量已满。

上面的这个方法就体现了贪心算法的局部最优性,迭代进行,每次都选择当前剩余物品中性价比最高的物品。

Minimum Cost Spanning Trees(Kruskal)

贪心法一个比较经典的使用场景是最小生成树。

最小生成树的定义:设 G = (V, E) 是一个无向连通图(connected undirected graph),并且每条边都有其权重。G 的一个生成树 (V, T)G 的一个子图,并且是一个树,也就是说没有回路。如果 T 中每条边的权重之和是最小的,那么就称 (V, T) 是最小代价生成树(Minimum Cost Spanning Trees),或者最小生成树(Minimum Spanning Trees)。

Kruskal 的方法原理是,维护一个由几个生成树组成的森林,然后将森林中的生成树逐渐合并,直到将所有生成树合并成一个树,这个树就是最小生成树。

算法的具体步骤分为两步:

  1. G 中所有的边按递增的顺序排序;
  2. 迭代选择图中的边,每次都选择最短的一条边:对于有序列表中的每一条边,如果这条边没有与 T 中的边构成回路,就将其放入 T 中;否则就丢弃这条边。

下面给出一个列子:

MST_Kruskal

实现

Kruskal 的最小生成树算法的实现需要使用并查集(Disjoint Set) 来判断当前的树中是否包含回路。

并查集相当于是一个森林,通常由数组实现,disjoint_set[i] 表示元素 i 的父节点,如果父节点是其本身,那么就意味着这个节点是其所在的树的根节点。初始阶段,每个节点的父节点都是其本身,然后通过合并而形成一个树。

并查集常用的有两种操作:查,找到目标元素所在的树的根节点;并,将两个两个不同的树合并成一个树。

Kruskal's algorithm:

Kruskal's_algorithm

上图给出了该算法实现的伪代码,其中的 MAKESET() 是指初始化一个并查集。

正确性

下面证明 Kruskal 算法的正确性。

需要使用到的定理:
If T is a tree with n vertices, then
(a) Any two vertices of T are connected by a unique path.
(b) T has exactly n − 1 edges.
(c) The addition of one more edge to T creates a cycle.

使用数学归纳法求证,对最小生成树所包含的边的集合 T 的大小进行归纳:

贪心问题的贪心选择性的证明常用数学归纳法求证。

最初,T = { } ,那么 T 显然是 T* 的一个子集。其中 T* 是最小生成树 G* = (V, T*) 中的边的集合。

假设 \(T\subset T^*\)。随后,在使用Kruskal算法添加一条边 e = (x, y)T 中之前,令 X 为包含节点 x 的子树节点的集合。

\(T' = T\cup \{e\}\),可以证明 T' 依然是 T* 的一个子集:

i. 如果 \(e\in T^*\),那么 \(T'\subset T^*\) 显然成立。
ii. 如果 \(e\notin T^*\),则:
根据上面的定理,\(T^* \cup \{ e\}\) 中会包含一个回路,而 e 正是这个回路中的一条边。而 e = (x, y) 连接了 X 中的一个节点和 V-X 中的一个节点。那么 T* 中也必然存在一条边 e' = (w, z) ,其中 \(w\in X, z \in V-X\),才能在 T* 中形成回路。
那么有 \(cost(e') \ge cost(e)\),否则根据Kruskal算法 e' 会先于 e 加入 T。现在构造 \(T^{**} = (T^* - \{e'\}) \cup \{e\}\),那么 \(T'\subset T^{**}\)
此外,\(T^{**}\) 也是包含最小生成树中所有边的集合,因为 e 是连接 X 中的节点与连接 V - X 中的节点的最短路径。
\(T\cup \{e\}\) 依然是最小生成树中所有边的集合的子集。

那么根据归纳法,通过Kruskal算法添加边得到的集合一定都是最小生成树所有边的集合的子集,直到这个集合的边能够连通 V 中的所有节点,得到最小生成树。

Minimum Cost Spanning Trees(Prim)

解决最小生成树问题还有另一个算法——Prim's algorithm。这个算法与Kruskal的算法完全不同,但同样也是使用贪心的策略进行决策。

Prim的算法以图中的任意一点为起点来生成最小生成树。令 G=(V, E) 为一个连通无向图,将 V 中节点编号为 {1, ..., n},初始时,令 X = {1}, Y = {2, ..., n},算法依然是迭代进行,每次迭代添加一条边,而这条边是节点 \(x\in X, y\in Y\) 的最短路径 (x, y)。添加这条边到最小生成树中所有边的集合 T 中,并且将节点 y 从集合 Y 中移除并加入到集合 X。不断迭代,直到集合 Y 为空集,得到最小生成树。

步骤如下:

  1. \(T\gets \{\}, X\gets \{1\}, Y\gets V - \{1\}\)
  2. (x,y) 为集合 X 到集合 Y 中节点的最短路径,则 \(X\gets X\cup \{y\}, Y\gets Y-\{y\}, T\gets T\cup \{(x,t)\}\)
  3. 重复步骤2,直到 \(Y=\{\}\)

下面给出一个例子:

MST_Prim

实现

Prim的算法使用邻接矩阵来实现,即矩阵 c[i,j] 表示边 (i, j) 的长度。然后使用 boolean 类型的一维数组 X[n], Y[n] 来表示集合 X, Y:

Prim_algorithm

上图为Prim算法的伪代码。

正确性

下面证明Prim算法的正确性。

同样的,使用归纳法进行证明。对集合 T 的大小进行归纳,可以证明 (X, Y) 是图 G 最小生成树的一个子树。

起初,T = { },上面的结论显然成立。

假设上面的结论在添加一条边 e = (x, y) 到集合 T 之前都是成立的,即 (X, T) 是最小生成树的一个子树,其中 \(x\in X, y\in Y\)。令 \(X' = X\cup \{y\}, T' = T\cup \{e\}\),那么可以证明,{X', T'} 依然是图 G 最小生成树的一个子树。

首先证明 (X', T') 是一个树。因为 \(y\notin X\),并且 (X, T) 本身是一个树,所以边 e = (x, y) 不会在 T' 中构成回路,即 (X', T') 是一个树。

现在证明 \(T' = T\cup \{e\}\)G 的最小生成树的一个子树。设图 G 的最小生成树为 G* = (V, T*)
i. 若 \(e\in T^*\) ,那么结论显然成立。
ii. 若 \(e\notin T^*\),那么根据前面的定理,\(T^*\cup \{e\}\) 必然包含一个回路。
这也就意味着 T* 中包含一条边 \(e' = (w, z), w\in X, z\in Y\)
那么根据 Prim 算法,只有\(cost(e') \le cost(e)\) 才能够使 \(e'\in T^*\),而这与 e 是集合 X 与集合 Y 节点间最短路径的前提矛盾。所以必然有 \(e\in T^*\)
因此 \(T' = T\cup \{e\} \subset T^*\)

综上,使用Prim算法得到的集合 (X, T) 是图 G 最小生成树的一个子树,直到 Y = {} 时,得到最小生成树。

Huffman编码

贪心法的另一个比较经典的使用案例是用于文件压缩的Huffman编码。

假设现在有一个文件,由字符串组成。现在希望尽可能多地压缩这个文件,并且能够轻易还原。假设文件中的字符集合为 \(C = \{c_1, ..., c_n\}\),令 \(f(c_i)\) 为字符 \(c_i\) 在文件中出现的频率,也就是出现的次数。

以固定长度的比特串来表示每个字符,也就是字符编码,那么文件的大小就取决于文件包含的字符总数。

然而,为了减小文件的大小,因为文件中某些字符出现的频率会远高于其他字符,所以可以考虑使用变长(variable-length)的编码方式,并且为出现次数比较高的字符分配更短的比特进行编码。

在变长编码中,需要注意某个字符的编码一定不能是另一个字符编码的前缀,这样的编码方式称为前缀编码(prefix codes)。比如,如果对字符 'a' 的编码为 "10", 对字符 'b' 的编码为 "101" 那么在扫描文件的时候就会出现二义性,也就是说 "10" 应该解释为字符 'a' 还是字符 'b' 的前缀呢?

如果编码方式满足了这样的限制,那么扫描文件时可以用下面的方式进行正确解码。使用一个完全二叉树,每个节点连接子节点的两条边分别标记为0,1,树的叶子节点就表示一个字符,而从根节点到叶子节点的路径上的01序列就是该字符的编码。

通过Huffman算法,能够构造出满足非前缀编码的编码方式,并且能够将文件的大小压缩到最小。

Huffman算法也是迭代进行的,令 C 表示所有字符的集合,选择出现频率最低的两个字符 \(c_i, c_j\),创造一个新的节点 c 作为这个节点父节点,并且令 c 的出现频率为这两个子节点出现频率之和。然后从集合 C 中移除节点 \(c_i, c_j\),并加入节点 c。重复这样的步骤,直到集合 C 只包含一个根节点。

Huffman_algorithm

上图为Huffman算法的伪代码。

下面看一个例子,假设一个文件由字符 'a', 'b', 'c', 'd', 'e' 组成,并且假设每个字符出现的频率分别为:f(a) = 20, f(b) = 7, f(c) = 10, f(d) = 4, f(e) = 18,找到一种编码方式使文件的大小压缩到最小。

使用Huffman算法可以构造出如下的完全二叉树:

Huffman_example

从图中可以看出每个字符的编码:e(a) = 00, e(b) = 111, e(c) = 10, e(d) = 110, e(e) = 01,这样的变长编码能够满足非前缀编码限制,所以也不存在二义性,能够被正确解码。

如果使用固定长度的编码方式,那么每个字符至少需要3比特才能编码,那么文件的长度也就是 3*(20 + 7 + 10 + 4 + 18) = 177 bits;如果使用这里得到的编码方式进行编码,那么文件的长度就是 2 * 20 + 3 * 7 + 2 * 10 + 3 * 4 + 2 * 18 = 129 bits,这样可以将文件压缩27%。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/810461.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Nuxt.js 应用中的 page:finish 钩子详解

title: Nuxt.js 应用中的 page:finish 钩子详解 date: 2024/10/9 updated: 2024/10/9 author: cmdragon excerpt: page:finish 是 Nuxt.js 中用于处理页面加载完成事件的钩子,特别是与 Suspense机制相关。这个钩子允许开发者在页面加载完成后执行自定义操作,以优化用户体验…

智驾仿真测试实战之自动泊车HiL仿真测试

1.引言汽车进入智能化时代,自动泊车功能已成为标配。在研发测试阶段,实车测试面临测试场景覆盖度不足、效率低下和成本高昂等挑战。为解决这些问题,本文提出一种自动泊车HiL仿真测试系统方案,可大幅度提升测试效率及测试场景覆盖度、缩短测试周期、加速产品迭代升级。2.自动…

直接跳转接入在线客服系统

PC网站或H5网站可以通过多种方式接入客服系统,直接访问或跳转聊天链接,是最简单的一种方式 很多人说,我这里的需求是根据xxxx规则进入某个客服(巴拉巴拉一大堆) 这个是前端自行处理的,你们随意任何规则,最终跳到我们这边指定的聊天连接上就行了 至于你们的规则是什么,我…

如何自己动手实现一个图片解答小助手

本文介绍了如何自己动手实现一个图片解答小助手。有一张图片如下所示:Kimi上有一个功能,就是解析图片内容,给出回答:这样可以用于拍照向AI提问的场景,我自己也有这方面的需求,因此动手实践了一下。 自己动手实现的效果如下所示:那么自己如何实现呢? 可以通过添加一个OC…

轻量虚拟机 multipass

multipass listmultipass info multipass shell 后续就可以 shell了

传奇霸业网页游戏单机版安装教程+GM后台+无需虚拟机

今天给大家带来一款单机游戏的架设:传奇霸业网页游戏。 另外:本人承接各种游戏架设(单机+联网) 本人为了学习和研究软件内含的设计思想和原理,带了架设教程仅供娱乐。教程是本人亲自搭建成功的,绝对是完整可运行的,踩过的坑都给你们填上了。如果你是小白也没问题,跟着教…

go抽象封装是一种简化认知的手段

通过 Kubernetes 看 Go 接口设计之道 原创 蔡蔡蔡菜 蔡蔡蔡云原生Go2024年10月01日 08:30 广东解耦依赖底层 在 Kubernetes 中能看到非常多通过接口对具体实现的封装。 Kubelet 实现了非常多复杂的功能,我们可以看到它实现了各种各样的接口,上层代码在使用的时候并不会直接实…

5.4 5.5

5.4 求解下列非线性规划:点击查看代码 import numpy as np from scipy.optimize import minimize def objective(x): return -np.sum(np.sqrt(x) * np.arange(1, 101)) def constraint1(x): return x[1] - 10 def constraint2(x): return 20 - (x[1] + 2*x[2]) def c…

升轨和降轨地表形变监测结果合成处理方法

一般情况下,InSAR处理得到的形变结果是视线方向形变(LOS)。在处理时候,输出结果可以选择垂直、坡向方向形变,甚至自定义方向上的形变,但这种结果往往是通过简单的勾股弦方式计算得到,理论精度不是很可靠。要想得到更精确的垂直向形变,甚至水平向(东西向、南北向)形变…

无线电通信卡:9-基于DSP TMS320C6678+FPGA XC7V690T的6U VPX信号处理卡

6U 6槽 VPX , AI加速计算 , 板 ,GPU计算主 图像处理卡 , 无线电通信一、概述 本板卡基于标准6U VPX 架构,为通用高性能信号处理平台,系我公司自主研发。板卡采用一片TI DSP TMS320C6678和一片Xilinx公司Virtex 7系列的FPGA XC7V690T-2FFG1761I作为主处理器,Xilinx 的Aritex …

vue 前端导出 excel

npm install xlsx-js-style import XLSX from xlsx-js-style;//导出数据 exportD(title,data,fileName){ title=["标题1","标题2","标题3","标题4","标题5","标题6"]; data=[["数据1…

[智能网联汽车/数据标准/法规政策] 标准解读:GB/T 44464-2024《汽车数据通用要求》

0 引言随着智能技术的不断发展,智能网联汽车作为新时代移动智能终端的代表,正引领着汽车产业向智能化、网联化深刻转型与升级。智能网联汽车与云端服务器、移动端、车端等设备存在大量的数据交互,包括车辆运行数据、用户个人信息等。缺乏对这些数据实施的有效监管与控制,将…