贪婪算法(Greedy Algorithms)

一,什么是贪婪算法

解决最简化问题的演算法,其解题过程可看成是由一连串的决策步骤所组成,而每一步骤都有一组选择要选定。

贪婪演算法的特性是 每一次选择都采取区域最佳接(locally optimal solution),而透过每一个区域最佳解最后综合成为全域最佳解(globally optimal solution)从而将问题解决

  • 一个贪婪算法在每一决策步骤总是选定当下看来最好的选择
  • 贪婪算法并不保证总是得到最佳解,但在有些问题可以得到最佳解

二,最短路径

找出下面多级图的最短路径
在这里插入图片描述
则使用贪婪算法,从S到T的最短路径为:1+2+5 = 8

我们再来看一个比较复杂的例子:
在这里插入图片描述
我们是用贪婪算法得到的最短路径为:(S - A - D - T) 1+4+18 = 23
而真实的最短路径为:(S - C - F - T) 5+2+2 = 9
可以看出又是使用贪婪算法得出的最终路径并非是真实的最短路径。

三,使用贪婪解题策略的演算法

  • 活动选择(activity-selection)演算法
  • 揹包(Knapsack)演算法
  • Huffman编码演算法
  • Kruskal最小扩张树演算法
  • Prim最小扩张树演算法
  • Dijkstra最短路径演算法

3.1 活动选择问题

  • 假设有n活动提出申请要使用一个场地,而这场地在同一时间点时最多只能让一个活动使用
  • 从这n个活动选一组数量最多,且可以在这场地举办的活动集。
  • 假设活动 ai 其提出申请使用场地的时段为半开半闭的区间 [ si, fi )

一个活动选择问题

假设我们有一集合 S = { a1, a2, …,an },其中有 n 个行销活动

  • 对于每一个活动 ai,其开始时间为 si结束时间为 fi(当 0 ≤ ai < fi < ∞)
  • 活动 ai在半开放时间间隔内发生 [ si, fi )

如果间隔[si,fi)和[sj,fj)不重叠,则活动ai和aj是兼容的(compatible)

  • 如果si ≥ fi 或 si ≤ fi,则说 ai和aj 是兼容的(compatible)

活动选择问题是选择相互兼容的活动的最大子集。
在这里插入图片描述
我们假设活动按完成时间单调递增的顺序排序:f1 ≤ f2 ≤ f3 ≤ … ≤ fn-1 ≤ fn
解题步骤如下:

  • 我们首先对这个程序制定一个动态规划解,其中我们将两个子问题的最优解结合起来,形成原始问题的最优解。
  • 然后我们将观察到,我们只需要考虑一个选择——贪婪的选择——并且当我们做出贪婪的选择时,其中一个子问题被保证为空,因此只剩下一个非空的子问题。

活动选择问题的最优子结构

  • Sij= { ak∈S : fi ≤ sk < fk ≤ sj }
    因此,Sij 是 S 中活动的子集,可以在活动ai结束之后开始,在活动aj开始之前结束。
  • 若 f0 = 0 和 sn+1 = ∞;
    则 S = S0,n+1(0 ≤ i, j ≤ n+1.)
  • Sij = Sik ∪ {ak} ∪ Skj

递归解决方案
Sij = Sik ∪ {ak} ∪ Skj
c[i, j] = c[i, k] + c[k, j] + 1
在这里插入图片描述
活动选择问题
P(A) 表示 A 为给定的活动集的问题,S 表示 P(A) 的最优解。对于 A 中的任何活动 ai,我们有

  1. ai ∈! S → S 是 P(A{ai}) 的最优解
  2. ai ∈ S → S {ai} 是 P(A\N[i]) 的最优解,但不一定是 P(A{ai}) 的最优解
    在这里插入图片描述
    ↑ 每一条横线段代表一个活动,其横向位置代表了发生的时段,若纵向有交叉的部分这说明两时间冲突。
    在P(A)的最优解中,A 中包含什么样的活动 ai:

那么,我们可以做到更好吗?

动态规划(Dynamic programming): O(n2)
贪婪的解决方案 - 有没有办法反复做出局部决定?

  • 关键:我们还是希望最终得到最优解
    在这里插入图片描述

3.2 贪婪选择(Greedy options)

1,选择最早开始的活动,i.e. min{s1, s2, s3, …, sn}?
在这里插入图片描述
↓ 可以看出,选择的不是最优解
在这里插入图片描述
,2,选择时间最短的活动,i.e. min{ f1 - s1, f2 - s2, f3 - s3, …, fn - sn }?
在这里插入图片描述
↓ 可以看出,选择的也不是最优解
在这里插入图片描述
3,选择冲突次数最少的活动(与其共同发生的活动称为冲突活动)
在这里插入图片描述
选择结果如下 ↓
在这里插入图片描述4,选择最早结束的活动,i.e. argmin{ f1, f2, f3, …, fn }?
在这里插入图片描述
蓝色是我们选择出的活动,而红色是与蓝色冲突的活动
在这里插入图片描述
然后把红色活动删掉,并且选择出第二早结束的活动(再继续上边的操作:删除冲突活动)
在这里插入图片描述
最终,经过选择,会留下以下4个活动
在这里插入图片描述

3.3 将动态规划解转化为贪婪解

考虑任何非空子问题Sij,并让am成为Sij中完成时间最早的活动:fm = min { fk : ak ∈ Sij}
则有如下性质:

  • 活动am用于Sij中相互兼容活动的一些最大子集。
  • 子问题Sim为空,因此选择am会使子问题Smj成为唯一可能非空的子问题。

四,高效的贪婪算法

4.1 贪婪算法的概述及特点

一旦你确定了一个合理的贪婪算法(reasonable greedy heuristic):

  • 证明它总是给出正确的答案
  • 可以开发有效的解决方案

贪婪方法概述

  • 贪婪法则挑选一项活动来安排
  • 将该活动添加到答案中
  • 删除该活动和所有冲突的活动,称为 A ’
  • 重复操作A ’,直到A ’为空集合

贪婪算法的特点

  • 贪婪选择特性:通过局部最优(optimal)选择可以得到全局最优解。
  • 最优子结构:在子问题的最优解范围内的问题的最优解。

4.2 设计贪婪算法

1,抛出优化问题

  • as one in which we make a choice
    做出选择
  • and with one subproblem to solve
    并且解决一个子问题

2, 证明对原来的问题总是有一个最优解,使贪婪的选择总是安全的

3, 在做出贪婪的选择之后

  • 如果我们将子问题的最优解与我们所做的贪婪选择相结合,我们就得到了原问题的最优解。

一个活动选择的例子:
问题:选择相互兼容的活动的最大子集。
在这里插入图片描述
我们假设活动按结束时间的单调递增顺序排序:f1 ≤ f2 ≤ f3 ≤ … ≤ fn-1 ≤ fn
为了方便观察可以换个简易图,如下:
在这里插入图片描述
很容易看出,相互兼容(不冲突)活动的子空间有:

  • {a1; a4; a8; a11}
  • {a2; a4; a9; a11}
  • {a3; a9; a11}

4.3 递归贪婪算法

'递归活动选择器(s, f, k, n)'
RECURSIVE-ACTIVITY-SELECTOR(s, f, k, n) 
1 m ← k +1 
2 while m ≤ n and s[m] < f [k]       # Find the first activity in Sij 
3 	do m ← m + 1 
4 if m ≤ n 
5	then return {am} ∪ RAS(s, f, m, n) 
6 	else return 0

递归活动选择器对11个活动的操作
在这里插入图片描述
在这里插入图片描述

4.4 迭代贪婪算法

'贪婪-活动-选择器'
GREEDY-ACTIVITY-SELECTOR(s, f) 
1 n ← s.length 
2 A ← {a1} 
3 k ← 1 
4 for m ← 2 to n 
5 	do if s[m] ≥ f[k] 
6 		then A ← A ∪ {am} 
7 			k ← m 
8 return A

五,贪婪策略的要素

  1. 确定问题的最优子结构;
  2. 开发递归解决方案;
  3. 证明如果我们做出贪婪的选择,那么只剩下一个子问题;
  4. 证明贪婪的选择总是安全的;(步骤3和4可以按任意顺序进行。)
  5. 开发实现贪婪策略的递归算法;
  6. 将递归算法转换为迭代算法。

另一方面,我们可以在考虑贪婪选择的情况下,设计出最优的子结构。

5.1 贪婪与动态规划

  • 揹包问题
  • 0-1揹包问题

5.1.1 揹包问题

定义

  • 给定一个最大载重容量(capacity)为 W 的揹包,以及n个可以放入揹包的物品,其中第i个物品重为wi > 0价格为pi > 0
  • 目标:找出 x1,…,xn最大化 Σ1≤i≤n pixi
  • 限制条件为: Σ1≤i≤n wixi ≤ W,其中 0≤xi≤1,1≤i≤n

对定义进行整理得出

  • n个物件,揹包容量为 W
    – 每个物件,重量 wi > 0
    – 每个物件,利润 pi > 0
  • 最大限度 Σ1≤i≤n pixi
  • 限制条件 Σ1≤i≤n wixi ≤ W(0≤xi≤1,1≤i≤n)

5.1.2 揹包演算法

Algorithm 揹包演算法

  • Input: 揹包的最大容量W,以及可以放入揹包的n个物品的非负重量wi价格pi
  • Output: 介于0与1之间的x1,…,xn分别代表第1个,…,第n个物品放入揹包中的零碎部份。可以最大化 Σ1≤i≤n pixi,并且满足 Σ1≤i≤n wixi ≤ W(0≤xi≤1,1≤i≤n)

1,将pi/wi由大至小排序。
2,根据此排序来将物品依序尽可能地放入揹包中,直至揹包容量W用完为止。
在这里插入图片描述
揹包演算法时间复杂度

  1. 依 pi/wi由大至小排序: O(n log n)
  2. 将物品依序放入揹包: O(n)

总时间复杂度: O(n log n)

5.1.3 0-1揹包问题

定义

  • 给定一个最载容量(capacity)为W的揹包,以及n个可以放入揹包的物品,其中第i个物品的重量为 wi > 0价格为 pi > 0
  • 目标:目标:找出 x1,…,xn最大化 Σ1≤i≤n pixi
  • 限制条件为: Σ1≤i≤n wixi ≤ W,其中 xi=0或1,1≤i≤n

揹包问题 与 0/1揹包问题的不同点在于:

  • 在0/1揹包问题中,xi只能是0或1
  • 而在揹包问题中,0 ≤ xi ≤ 1

贪婪策略不适用于0-1揹包
在这里插入图片描述
揹包中是否包含物品

  • 我们必须将解决方案与包含物品的子问题和排除该物体的子问题的解决方案进行比较。

以这种方式表述的问题会产生许多重叠的子问题——动态规划的一个特征
在这里插入图片描述

5.2 Huffman编码

字元编码(character coding)可以分为:

  • 固定长度编码: 如ACSII、Unicode
  • 可变长度编码: Huffman code

Huffman编码以字首码(prefix code)方式达到字元编码最佳资料压缩(optimal data compression)

  • 字首码 (prefix code): 任何字元编码一定不是其他字元编码的字首(prefix)。
  • 可以使用二元树来呈现,达到简单编码(encoding)与解码(decoding)的功能。

5.2.1 Huffman编码范例

假设给定一个仅用到a, b, c, d, e五个字元的文件,现在想针对五个字元进行编码,以下是可能的固定长度编码与可变长度的Huffman字首码。

字首码(Prefix code) 让出现频率较高字元的编码较短, 以达到使用最少位元就可以将所有资料储存的目标。
在这里插入图片描述
100000个字符的数据文件只包含字符a–f

  • 为每个字符分配一个3位码字,文件编码为300,000位
  • 使用所示的可变长度代码,我们可以将文件编码为224,000位(节省大约25%)

5.2.2 树对应于编码方案

在这里插入图片描述

5.2.3 构造哈夫曼码

HUFFMAN( C ) 
1 n ← |C| 
2 Q ← C 
3 for i ← 1 to n – 1 
4 	do allocate a new node z 
5 		left[z] ← x ← EXTRACT-MIN(Q) 
6 		right[z] ← y ← EXTRACT-MIN(Q) 
7 		f[z] ← f[x] + f[y] 
8 		INSERT(Q, z) 
9 return EXTRACT-MIN(Q)

Huffman编码演算法时间复杂度

  • 行2: O(n)建立优先伫列Q
  • 行3-8: for回圈一共执行n-1次,而且回圈中的优先伫列操作均为O(log n)复杂度,因此整个回圈具有O(n log n)的复杂度

总时间复杂度: O(n log n)

5.2.4 Huffman演算法的步骤:

在这里插入图片描述
在这里插入图片描述

六,使用贪婪解题策略的演算法

  • 活动选择(activity-selection)演算法
  • 揹包(Knapsack)演算法
  • Huffman编码演算法
  • Kruskal最小扩张树演算法
  • Prim最小扩张树演算法
  • Dijkstra最短路径演算法
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章