首页 > 其他分享 >贪心法

贪心法

时间:2024-10-09 11:49:02浏览次数:8  
标签:字符 最小 生成 算法 节点 贪心

算法导论

这个文档是学习“算法设计与分析”课程时做的笔记,文档中包含的内容包括课堂上的一些比较重要的知识、例题以及课后作业的题解。主要的参考资料是 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%。

标签:字符,最小,生成,算法,节点,贪心
From: https://www.cnblogs.com/TheFutureIsNow/p/18453925

相关文章

  • Day 24 贪心算法part02| LeetCode 122.买卖股票的最佳时机II,55.跳跃游戏,45.跳跃游戏II
    122.买卖股票的最佳时机II局部最优:每天的正利润全局最优:每天的正利润之和121.买卖股票的最佳时机classSolution{publicintmaxProfit(int[]prices){intres=0;for(inti=1;i<prices.length;i++){i......
  • 9-贪心算法
    参考:代码随想录题目分类大纲如下:贪心算法理论基础什么是贪心?贪心的本质是选择每一阶段的局部最优,从而达到全局最优。贪心的套路(什么时候用贪心)贪心算法并没有固定的套路,说白了就是常识性推导加上举反例。靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,如果不可行,可能需要......
  • 【贪心】【二分】[NOIP2015]跳石头
    https://ac.nowcoder.com/acm/contest/22353/C正确的思路是二分查找+贪心。具体来说,可以通过二分来猜测一个最小的跳跃距离,然后通过贪心算法判断是否可以通过移除石头使所有的跳跃都满足这个最小跳跃距离。这种方法的核心是逐步逼近最优解,而不是直接删除前m小的元素。细节:在......
  • 代码随想录算法训练营 | 贪心算法:455.分发饼干,376. 摆动序列,53. 最大子序和
    455.分发饼干题目链接:455.分发饼干文档讲解︰代码随想录(programmercarl.com)视频讲解︰分发饼干日期:2024-10-02想法:大饼干喂大孩子Java代码如下:classSolution{publicintfindContentChildren(int[]g,int[]s){Arrays.sort(g);Arrays.sort(s);......
  • 9564 Work Scheduling 结构体排序 优先队列 最小堆 贪心
    解决思路 排序:首先将所有工作按照截止时间 D_i 进行排序。 优先队列:使用一个最小堆来存储当前选择的工作的利润。 选择工作:遍历所有工作,如果当前工作的截止时间大于堆的大小,则将该工作加入堆中;否则,如果当前工作的利润大于堆顶的利润,则替换堆顶的工作。#include......
  • E60 树形DP+贪心 P3574 [POI2014] FAR-FarmCraft
    视频链接:   P3574[POI2014]FAR-FarmCraft-洛谷|计算机科学教育新生态(luogu.com.cn)//树形DP+贪心O(nlogn)#include<iostream>#include<cstring>#include<algorithm>usingnamespacestd;constintN=500005;inthead[N],to[N<<1],ne[N<......
  • 【算法】贪心+堆排序实现大根堆及标准库容器类的融合使用
    ......
  • 【代码随想录Day27】贪心算法Part01
    理论基础题目链接/文章讲解:代码随想录视频讲解:贪心算法理论基础!_哔哩哔哩_bilibili455.分发饼干题目链接/文章讲解:代码随想录视频讲解:贪心算法,你想先喂哪个小孩?|LeetCode:455.分发饼干_哔哩哔哩_bilibili一开始使用了双重循环,时间复杂度为......
  • Day 23 贪心算法part01| LeetCode 455.分发饼干,376.摆动序列,53.最大子序和
    455.分发饼干455.分发饼干classSolution{publicintfindContentChildren(int[]g,int[]s){Arrays.sort(g);Arrays.sort(s);intindex=s.length-1;intcount=0;for(inti=g.le......
  • leetcode刷题day27|贪心算法Part01(455.分发饼干、376. 摆动序列、53. 最大子序和)
    前言:贪心的本质选择每一阶段的局部最优,从而达到全局最优。455.分发饼干思路:局部最优-大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个;全局最优:喂饱尽可能多的小孩。可以尝试使用贪心策略,先将饼干数组和小孩数组排序,然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计......