贪心算法 II

上一篇文章中介绍了贪心算法,并给出了几个题目的分析证明,其中“Gas Station”那道题的难度比较大,但是让我们很好的体会到贪心算法的用武之地,同时也体会到证明的重要性,贪心算法并不能保证得到的一定是最优解,一定要得到证明方可放心使用。本篇继续介绍贪心算法实例。

1. 活动选择问题

假定有一个n个活动的集合S={a1,a2,...,an},这些活动使用同一个资源,而这个资源在某个时刻只能供一个活动使用。每个活动ai都有一个开始时间si和结束时间fi,其中0<=si<fi<+inf。如果被选中,任务ai发生在半开时间区间[si,fi)期间。如果两个活动ai和aj满足[si,fi)和[sj,fj)不重叠,则称它们是兼容的。在活动选择问题中,我们希望选出一个最大兼容活动集。假设活动已按结束时间单调递增顺序排序:f1<=f2<=f3<=....<=fn-1<=fn。

贪心策略:优先选择尽早结束的活动。

定理:考虑任意非空子问题Sk,令am是Sk中结束最早的活动,那么am在Sk的某个最大兼容活动子集中。

证明:令Ak是Sk的一个最大兼容子集,且aj是Ak中结束时间最早的活动。若aj = am,则问题得证。若aj != am,令集合Ak‘ = Ak-{aj} U {am},即将Ak中的aj替换为am。Ak'中的活动都是不相交的。由于|Ak'| = |Ak|,因此得出结论,Ak'也是Sk的一个最大兼容活动子集,且它包含am。

伪代码:

GREEDY-ACTIVITY-SELECTOR(s,f)

n = s.length

A={a1}

k = 1

for m = 2 to n

      if s[m] >= f[k]

            A = AU{am}

            k = m

return A

注意其中的s[m]表示活动m的开始时间

《算法导论》一书中对该问题的最优子结构以及与动态规划的比较有详细介绍,本文只关注贪心算法,不再涉及多余内容。在以后动态规划的相关文章中再做探讨。

2. 赫夫曼编码

根据每个字符的出现频率,根据贪心策略构造出字符的最优二进制表示。

前缀码:没有任何码子是其他码字的前缀。我们只考虑前缀码。

给定一棵对应前缀码的树T,我们可以计算其代价B(T):


其中C是字母表,c是字母表中的字符,c.freq表示c在文件中出现的频率,d_T(c)表示c的叶节点在树中的深度(也是字符c的码字的长度)。

算法过程:从|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)

Java代码如下:

import java.util.*;
public class Test{  
public static Node huffman(int[] C){
	int n = C.length;
	PriorityQueue<Node> Q = new PriorityQueue<Node>();
	for(int f:C) Q.add(new Node(f));
	for(int i = 0;i < n-1;i++){
		Node z = new Node(0);
		Node x = Q.poll();
		Node y = Q.poll();
		z.left = x;
		z.right = y;
		z.freq = x.freq+y.freq;
		Q.offer(z);
	}
	return Q.poll();
}
public static void traverse(Node root){
	if(root == null) return;
	traverse(root.left);
	System.out.print(root.freq+" ");
	traverse(root.right);
}
public static void main(String[] args){  
	int[] C = {45,13,12,16,9,5};
	Node root = huffman(C);
	traverse(root);
}
static class Node implements Comparable<Node>{
	public int freq;
	public Node left;
	public Node right;
	Node(int freq){this.freq = freq;}
	public int compareTo(Node b){
		return this.freq - b.freq;
	}
}
}  

证明过程可以参考《算法导论》


3.找零问题

考虑用最少硬币找n美分零钱的问题。假定每种硬币的面额都是整数。

a.设计贪心算法求解找零问题,假定有25美分、10美分、5美分和1美分4种面额的硬币。证明你的解法能够找到最优解。

这时算导上的一道思考题,比较简单。贪心策略:每次从最大的开始选,直到找不开,再选次大的。为什么可以找到最优解,也很简单。从条件中可以看到,每个较大面额的硬币至少要两张比它小的面额能够代替它,因此,如果较大的硬币没有找到最大数,最后硬币总数肯定是要更大的。

b与a类似,略。

c. 设计一组硬币面额,使得贪心算法不能保证得到最优解。这组硬币面额中应包含1美分,使得对每个零钱值都能存在找零方案。

这个问题比较好,前面我们一直在证明贪心算法能够找到最优解,那么如何证明贪心算法找不到最优解呢?上面的证明中提到了一个非常重要的一点,每一个较大面额的硬币至少要两张比它小的硬币能够代替它,这是我们上面的结论得以成立的充分条件。因此,如果贪心算法不能得到最优解,那么就存在至少一枚硬币不能由至少2枚比其小面额的硬币代替。比如15,9,4,1其中15不能由2个9代替,而且对于来说,按贪心算法的话22 = 15+4+1+1+1,需要5枚;其实还可以22 = 9+9+4,只需要3枚就可以,贪心算法失效。

类似的还有01背包问题(要么全装入,要么不装)也不能用贪心算法,但是背包问题(可以选择装入一部分)可以用贪心算法。


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值