贪心算法
求解最优化问题的算法通常需要经过一系列的步骤,在每个步骤都面临多种选择。但许多最优化问题使用动态规划算法去做显然有些小题大做了。
贪心算法相比动态规划算法来说,具有更简单,更高效的特点。
贪心算法: 它在每一步都做出当时看起来最佳的选择,也就是说,其总是做出局部最优的选择,寄希望这样的选择能够导致全局最优解。
贪心算法并不能保证得到最优解,但是对很多问题确实可以求得最优解。
一、活动选择问题
- 假定有n个活动的集合 S={a1,a2,⋯,an} ,这些活动使用同一个资源,而这个资源在某个时刻只能供一个活动使用。
- 每个活动 ai 都有一个开始时间 si 和一个结束时间 fi ,其中 0⩽si<fi<∞ 。
- 如果被选中,任务 ai 发生在半开时间区间 [si,fi) 期间。如果两个活动 ai 和 aj 满足 [si,fi) 和 [sj,fj) 不重叠,则称他们是兼容的。
- 在活动选择问题中,我们希望选出一个最大兼容活动集。
假定活动已经按照结束时间的单调递增顺序排序,如下表中的数据所示:
递归贪心算法
我们可以绕过动态规划的方法而使用自顶向下的贪心算法来求解活动选择问题,现在我们可以设计一个直接的递归过程来实现贪心算法。
- 输入为两个数组s和k,表示活动的开始和结束时间,下标k指出要求解的子问题 Sk ,以及问题规模n;
- 返回 sk 的一个最大兼容活动集。
- 运行时间: Θ(n)
Recursive_activity_selector(s,f,k,n)
m = k + 1
while m<=n and s[m]<f[k]
m = m+1
if m <= n
return {a_{m}} and Recursive_activity_selector(s,f,m,n)
else return amptyset
上面已经假定输入的n个活动已经按结束时间的单调递增顺序排列好。同时为了方便初始化,添加了一个虚拟活动 a0 ,其结束时间 f0=0 ,这样子问题 S0 就是完整的活动集合S。
求解原问题即可调用Recursive_activity_selector(s,f,0,n)。
迭代贪心算法
上面递归贪心算法中,其以一个对自身的递归调用再接一次并集操作结尾,这里我们将尾递归过程改为迭代形式。
下面的程序将选出的活动存入结合A中,并将A返回。
运行时间同样为: Θ(n)
Greedy_activity_selector(s, f)
n = s.length
A = {a_{1}}
k = 1
for m = 2 to n
if s[m] >= f[k]
A = A and {a_{m}}
k = m
return A
二、贪心对动态规划
0-1背包问题
一个正在抢劫商店的小偷发现了n个商品,第i个商品价值 vi 美元,重 wi 磅, vi 和 wi 都是整数,这个小偷希望拿走价值尽量高的商品,但是他的背包最多能容纳W磅重的商品,W是一个整数。该如何拿商品?
这种问题为0-1背包问题,要么把它完整拿走,要么把它留下,不能只拿走一个商品的一部分,或者把一个商品拿走多次。
分数背包问题
设定与0-1背包问题是一样的,但是对每个商品,小偷可以拿走其一部分,而不是只能做出二元(0-1)选择。可以将0-1背包问题中的商品想象为金锭,分数背包问题中的商品想象为金砂。
比较
虽然两个问题相似,但是我们用贪心策略可以求解分数背包问题,而不能求解0-1背包问题;
对于0-1背包问题则用动态规划的方法求解。
三、赫夫曼编码
赫夫曼编码可以很有效地压缩数据,通常可以节省大约20%~90%的空间。
在二进制字符编码中,变长编码可以达到比定长编码好得多的压缩率,其思想是赋予高频字符短码字,赋予低频字符长码字。如下表所示:
前缀码
其中,编码的符号均为前缀码,即没有任何码字是其他码字的前缀。
在解码的过程中,如:001011101,可以唯一地解析为:0、0、101、1101,解码为aabe。
解码过程需要前缀码的一种方便的表示形式,以便我们可以容易地截取开始码字,一种二叉树表示可以满足这种需求,其叶节点为给定的字符,字符的二进制码用从根结点到该字符叶结点的简单路径表示。
构造赫夫曼编码
赫夫曼设计了一个贪心算法来构造最优前缀码,被称为赫夫曼编码。
- 假定C是一个n个字符的集合,而其中每个字符 c∈C 都是一个对象,其属性c.freq给出了字符的出现频率;
- 算法自底向上地构造出最优编码的二叉树T;
- 算法从 |C| 个叶节点开始,执行 |C|−1 个合并操作创建出最终的二叉树;
- 算法使用一个以属性freq为关键字最小优先队列Q,以识别两个最低频率的对象将其合并。
Huffman(C)
n = |C|
Q = C
for i = 1 to n-1
allocate a new node z
z.left = x = Extract_min(Q)
z.right = y = Extract_min(Q)
z.freq = x.freq + y.freq
Insert(Q,z)
return extract_min(Q)
例子: