Tenloy's Blog

(三) 贪心算法

Word count: 3.6kReading time: 12 min
2021/06/24 Share

一、引例 —— 钱币找零问题

贪心算法的思想非常简单且算法效率很高,在一些问题的解决上有着明显的优势。

假设有3种硬币,面值分别为1元、5角、1角。这3种硬币各自的数量不限,现在要找给顾客3元6角钱,请问怎样找才能使得找给顾客的硬币数量最少呢?

你也许会不假思索的说出答案:找给顾客3枚1元硬币,1枚5角硬币,1枚1角硬币。其实也可以找给顾客7枚5角硬币,1枚1角硬币。可是在这里不符合题意。在这里,我们下意识地应用了所谓贪心算法解决这个问题。

所谓贪心算法,就是 总是做出在当前看来是最好的选择(未从整体考虑)的一种方法。以上述的题目为例,为了找给顾客的硬币数量最少,在选择硬币的面值时,当然是尽可能地选择面值大的硬币。因此,下意识地遵循了以下方案:

(1)首先找出一个面值不超过3元6角的最大硬币,即1元硬币。
(2)然后从3元6角中减去1元,得到2元6角,再找出一个面值不超过2元6角的最大硬币,即1元硬币。
(3)然后从2元6角中减去1元,得到1元6角,再找出一个面值不超过1元6角的最大硬币,即1元硬币。
(4)然后从1元6角中减去1元,得到6角,再找出一个面值不超过6角的最大硬币,即5角硬币。
(5)然后从6角中减去5角,得到1角,再找出一个面值不超过1角的最大硬币,即1角硬币。
(6)找零钱的过程结束。

这个过程就是一个典型的贪心算法思想。

二、基本思想:

  • 贪心选择:所做的每一个选择都是当前状态下局部的最好选择。
  • 贪心算法:指从问题的初始状态出发,自顶向下,通过一系列的贪心选择来得到问题的解。

贪心策略总是做出在当前看来是最优的选择,也就是说贪心策略并不是从整体上加以考虑,它所做出的选择只是在某种意义上的局部最优解,而许多问题自身的特性决定了该问题运用贪心策略可以得到最优解或较优解。

(注意:贪心算法不是对所有问题都能得到整体最优解,但对范围相当广泛的许多问题它能产生整体最优解。但其解必然是最优解的很好近似解。)

贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。选择的贪心策略必须具备无后效性。

三、适用情况

3.1 适用前提

贪心策略适用的前提是:

严格意义上讲,要使用贪心算法求解问题,该问题应当具备以下性质:

  • 贪心选择性质
    • 所谓贪心选择性质,就是指所求解的问题的整体最优解可以通过一系列的局部最优解得到(换个说法:局部最优策略能导致产生全局最优解!)。所谓局部最优解,就是指在当前的状态下做出的最好选择。
    • 贪心法必须进行正确性证明:数学归纳法、反证法等
  • 最优子结构性质
    • 当一个问题的最优解包含着它的子问题的最优解时,就称此问题具有最优子结构性质。
    • 这个性质和动态规划法的一样,最优子结构性质是可用动态规划算法或贪心算法求解的关键特征。

注意:对于一个给定的问题,往往可能有好几种量度标准。初看起来,这些量度标准似乎都是可取的,但实际上,用其中的大多数量度标准作贪婪处理所得到该量度意义下的最优解并不是问题的最优解,而是次优解。

因此,选择能产生问题最优解的最优量度标准是使用贪婪算法的核心

实际上,贪心算法适用的情况很少。一般,对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,就可做出判断。

3.2 局限

  • 不能保证最后的解是最优的;
  • 不能求最大最小解问题;
  • 只能求满足某些约束条件的可行解范围。

四、Greed与DP

最优解问题大部分都可以拆分成一个个的子问题(多阶段决策问题),把解空间的遍历视作对子问题树的遍历,则以某种形式对树整个的遍历一遍就可以求出最优解,大部分情况下这是不可行的。

贪心算法和动态规划本质上是对子问题树的一种修剪,两种算法要求问题都具有的一个性质就是子问题最优性(组成最优解的每一个子问题的解,对于这个子问题本身肯定也是最优的)。

动态规划方法代表了这一类问题的一般解法,自底向上构造子问题的解,对每一个子树的根,求出下面每一个叶子的值,并且以其中的最优值作为自身的值,其它的值舍弃。

而贪心算法是动态规划方法的一个特例,可以证明每一个子树的根的值不取决于下面叶子的值,而只取决于当前问题的状况。换句话说,不需要知道一个节点所有子树的情况,就可以求出这个节点的值。由于贪心算法的这个特性,它对解空间树的遍历不需要自底向上,而只需要自根开始( 自顶向下),选择最优的路,一直走到底就可以了。

4.1 相同之处

  1. 都可将一个问题的解决方案视为一系列决策的结果(拆分一个个子问题)
  2. 都要求问题满足最优化原理
  3. 都要求状态满足无后效性(状态→|决策|→状态)

4.2 不同之处

一个问题分为多个阶段,每个阶段可以有n种决策,各个阶段的决策构成一个决策序列,称为一个策略。

这两种算法都是选择性算法,在进行决策的选择时:

  • 动态规划:试探性选择——每步所作的选择依赖于相关子问题的解。每一步要试探所有的可行解并将结果保存起来,最后通过回溯的方法确定最优解,其试探策略称为决策过程。可回退。所以,需要记录之前的所有最优解。
  • 贪心算法:贪心选择—仅在当前状态下做出最好选择,不依赖于子问题的解。优化了选择的策略,通过对候选解按照一定的规则进行排序,然后按照排好的顺序进行选择(将n个可选决策优化到了1个),选择过程中仅需确定当前元素是否要选取,与后面的元素是什么没有关系。不可回退。所以,上一步之前的最优解则不作保留。

前提是这个问题得具有贪心选择性质,需要证明(数学归纳法(第一、第二)),如果不满足那就只能使用动态规划解决。(一旦证明贪心选择性质,用贪心算法解决问题比动态规划具有更低的时间复杂度和空间复杂度。)

4.3 总结

从范畴上来看:Greedy ⊂ DP ⊂ Searching (贪心是动规的特例)。即所有的贪心算法问题都能用DP求解,更可以归结为一个搜索问题,反之不成立。

贪心算法所作的选择可以依赖于以往所作过的选择,但决不依赖于将来的选择,也不依赖于子问题的解,这使得算法在编码和执行的过程中都有着一定的速度优势。如果一个问题可以同时用几种方法解决,贪心算法应该是最好的选择之一。但是贪心算法并不是对所有的问题都能得到整体最优解或最理想的近似解,与回溯法等比较,它的适用区域相对狭窄许多,因此正确地判断它的应用时机十分重要。

五、基本步骤

一步一步地进行,常 以当前情况为基础根据某个优化测度作最优选择,而不考虑各种可能的整体情况,它省去了为找最优解要穷尽所有可能而必须耗费的大量时间。

它采用 自顶向下,以 迭代的方法做出相继的贪心选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题,通过每一步贪心选择,可得到问题的一个最优解,虽然每一步上都要保证能获得局部最优解,但由此产生的全局解有时不一定是最优的,所以贪心法不需要回溯

  1. 建立数学模型来描述问题。
  2. 把求解的问题分成若干个子问题。
  3. 对每一子问题求解,得到子问题的局部最优解。
  4. 把子问题的解局部最优解合成原来解问题的一个解。

六、程序设计

一般的算法设计模式如下:

1
2
3
4
5
6
从问题的某一初始解出发;
while(能朝给定总目标前进一步)
{
利用可行的决策,求出可行解的一个解元素;
}
由所有解元素组合成问题的一个可行解;

七、经典运用

  • 活动选择问题,
  • 再论背包问题,
  • 小船过河问题,
  • 区间覆盖问题,
  • 销售比赛,
  • 装箱问题,
  • 哈夫曼(Huffman Tree)编码算法
  • 求解最小生成树的克鲁斯卡尔(Kruskal)算法
  • 普利姆(Prim)算法
  • 求解图的单源最短路径的迪克斯特拉(Dijkstra)算法

7.1 马踏棋盘的贪心算法

【问题描述】

马的遍历问题。在8×8方格的棋盘上,从任意指定方格出发,为马寻找一条走遍棋盘每一格并且只经过一次的一条最短路径。

【贪心算法】

其实马踏棋盘的问题很早就有人提出,且早在1823年,J.C.Warnsdorff就提出了一个有名的算法。在每个结点对其子结点进行选取时,优先选择‘出口’最小的进行搜索,‘出口’的意思是在这些子结点中它们的可行子结点的个数,也就是‘孙子’结点越少的越优先跳,为什么要这样选取,这是一种局部调整最优的做法,如果优先选择出口多的子结点,那出口少的子结点就会越来越多,很可能出现‘死’结点(顾名思义就是没有出口又没有跳过的结点),这样对下面的搜索纯粹是徒劳,这样会浪费很多无用的时间,反过来如果每次都优先选择出口少的结点跳,那出口少的结点就会越来越少,这样跳成功的机会就更大一些。

7.2 背包问题(贪心非最优解)

下面是一个可以试用贪心算法解的题目,贪心解的确不错,可惜不是最优解 —— 背包问题

【问题描述】

有一个背包,背包容量是M=150。有7个物品,物品可以分割成任意大小。
要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。
物品 A B C D E F G
重量 35 30 60 50 40 10 25
价值 10 40 30 50 35 40 30

【问题分析】

目标函数: ∑pi 最大

约束条件是装入的物品总重量不超过背包容量:∑wi <= M(M=150)
(1)根据贪心的策略,每次挑选价值最大的物品装入背包,得到的结果是否最优?
(2)每次挑选所占重量最小的物品装入是否能得到最优解?
(3)每次选取单位重量价值最大的物品,成为解本题的策略。

值得注意的是,贪心算法并不是完全不可以使用,贪心策略一旦经过证明成立后,它就是一种高效的算法。

贪心算法还是很常见的算法之一,这是由于它简单易行,构造贪心策略不是很困难。可惜的是,它需要证明后才能真正运用到题目的算法中。

一般来说,贪心算法的证明围绕着:整个问题的最优解一定由在贪心策略中存在的子问题的最优解得来的。

对于例题中的3种贪心策略,都是无法成立(无法被证明)的,解释如下:

(1)贪心策略:选取价值最大者。反例:

W=30
物品:A B C
重量:28 12 12
价值:30 20 20
根据策略,首先选取物品A,接下来就无法再选取了,可是,选取B、C则更好。

(2)贪心策略:选取重量最小。它的反例与第一种策略的反例差不多。

(3)贪心策略:选取单位重量价值最大的物品。反例:

W=30
物品:A B C
重量:28 20 10
价值:28 20 10
根据策略,三种物品单位重量价值一样,程序无法依据现有策略作出判断,如果选择A,则答案错误。

Author:Tenloy

原文链接:https://tenloy.github.io/2021/06/24/greed.html

发表日期:2021.06.24 , 8:40 AM

更新日期:2024.04.07 , 8:02 PM

版权声明:本文采用Crative Commons 4.0 许可协议进行许可

CATALOG
  1. 一、引例 —— 钱币找零问题
  2. 二、基本思想:
  3. 三、适用情况
    1. 3.1 适用前提
    2. 3.2 局限
  4. 四、Greed与DP
    1. 4.1 相同之处
    2. 4.2 不同之处
    3. 4.3 总结
  5. 五、基本步骤
  6. 六、程序设计
  7. 七、经典运用
    1. 7.1 马踏棋盘的贪心算法
    2. 7.2 背包问题(贪心非最优解)