贪心算法

Posted tttfu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了贪心算法相关的知识,希望对你有一定的参考价值。

贪心算法具有最优子问题结构,它的特点是“短视”,每次选择对当前局面最有利的决策,来一步步获得最优解。


我个人认为,贪心不是一个具体的方法,而是一类方法,贪心算法的关键不在于想到,而在于正确性的证明。要证明一个贪心算法是正确的,需要证明我们可以把一个最优解逐步转化为我们用贪心算法所得到的解,而解不会更差,从而证明贪心算法得到的解和最优解是一样好的(显然,最优解不可能更好)。而要证明一个贪心算法是错误的,只需要找到一个反例就可以了。

通常情况下,证明贪心算法是正确的或者找到贪心算法的一个反例都不那么容易。

而且即使对于同一个问题,从不同角度的贪心算法的正确性也不尽相同。例如Dijkstra算法是著名的求单源图最短路径的贪心算法。
 
技术分享图片
如果我们也给出一个贪心算法,从源头开始每次选择最短的边继续走,直到走到,直到经过全部点或者无路可走。按照我们的算法,在上图中,A到C的最短路是A-B-C,它的长度是5,显然A-D-C才是真正的最短路。我们的贪心算法是错的。

所以一般对于一个问题来说,我们只讲这样一个贪心算法是错误的,而不说这个问题不能采用贪心算法——因为可能从别的角度设计出的贪心算法是正确。
======================================================================================================================
一个问题即使不能使用某个贪心算法,也可以通过贪心算法给出一个“还说得过去”的解,这也是贪心算法在现实中存在的意义之一。

基本的算法中贪心著名的贪心算法包括: Dijskstr单源图最短路径算法、Prim和Kruskal最小生成树算法、Huffman编码简单压缩算法等。

如果给贪心算法一个抽象地描述,我认为可以这样讲: 假设一些对象的集合S, 每个对象x对应一个收益payoff(x),对于任意S的子集T,我们有一个函数可以判断它是否合法isValid(T) ——它返回布尔值。并且这个函数通常有个性质,空集是合法的;如果 T合法,它的任意子集都合法;如果它非法,它的任意超集都非法。我们的目标是从 S中选取若干个对象,形成一个集合V,使得isValid(V) == true并且payoff(V)尽可能大。其中payoff(V)定义为V中所有对象的收益之和。贪心算法是这么解决这个问题的,从空集合开始,每次选一个payoff最大并且合法的对象x加入到V里面, V = VU{x}。

可见具备上述性质的问题实际上还是比较特殊的,而上述性质通常成为贪心选择性。

可见贪心选择是比较“短视”的,选取最优的一个元素,即使有多个,任选一个。而动态规划算法是从所有能达到当前状态的状态和决策中选取,所以从某种角度上讲,动态规划是枚举——只是聪明点的枚举罢了,它枚举的是所有状态以及该状态下的决策。而贪心只是单一的选择,盲目选择当前最优的决策。
 
贪心和动态规划算法的比较可见下表:
技术分享图片
本章主要讲述贪心算法,让我们一起来领略它的风采吧。
===================================================================================================================
最小生成树的Prim算法也是贪心算法的一大经典应用。Prim算法的特点是时刻维护一棵树,算法不断加边,加的过程始终是一棵树。

Prim算法过程:

一条边一条边地加, 维护一棵树。

初始 E = {}空集合, V = {任意节点}

循环(n – 1)次,每次选择一条边(v1,v2), 满足:v1属于V , v2不属于V。且(v1,v2)权值最小。

E = E + (v1,v2)
V = V + v2

最终E中的边是一棵最小生成树, V包含了全部节点。
以下图为例介绍Prim算法的执行过程。
技术分享图片
Prim算法的过程从A开始 V = {A}, E = {}
技术分享图片
选中边AF , V = {A, F}, E = {(A,F)} 
技术分享图片
选中边FB, V = {A, F, B}, E = {(A,F), (F,B)}
技术分享图片
选中边BD, V = {A, B, F, D},   E = {(A,F), (F,B), (B,D)}
技术分享图片
选中边DE, V = {A, B, F, D, E},   E = {(A,F), (F,B), (B,D), (D,E)}
 技术分享图片
选中边BC, V = {A, B, F, D, E, c},   E = {(A,F), (F,B), (B,D), (D,E), (B,C)}, 算法结束。
Prim算法的证明:假设Prim算法得到一棵树P,有一棵最小生成树T。假设P和T不同,我们假设Prim算法进行到第(K – 1)步时选择的边都在T中,这时Prim算法的树是P’, 第K步时,Prim算法选择了一条边e = (u, v)不在T中。假设u在P’中,而v不在。

因为T是树,所以T中必然有一条u到v的路径,我们考虑这条路径上第一个点u在P’中,最后一个点v不在P’中,则路径上一定有一条边f = (x,y),x在P’中,而且y不在P’中。
我们考虑f和e的边权w(f)与w(e)的关系:

若w(f) > w(e),在T中用e换掉f (T中加上e去掉f),得到一个权值和更小的生成树,与T是最小生成树矛盾。
若w(f) < w(e), Prim算法在第K步时应该考虑加边f,而不是e,矛盾。

因此只有w(f) = w(e),我们在T中用e换掉f,这样Prim算法在前K步选择的边在T中了,有限步之后把T变成P,而树权值和不变, 从而Prim算法是正确的。
请仔细理解Prim算法——时刻维护一棵生成树。我们的证明构造性地证明了所有地最小生成树地边权(多重)集合都相同!








































以上是关于贪心算法的主要内容,如果未能解决你的问题,请参考以下文章

程序员算法基础——贪心算法

贪心思想

算法基础--贪心算法

贪心算法学习,附由贪心算法引发的人生感悟。

算法贪心算法(0-1背包问题)

python常用算法——贪心算法,欧几里得算法