算法导论——贪心算法

贪心算法(greedy algorithm)是指,在每一步都做出当时看起来最佳的选择,也就是局部最优的选择,期望这样的选择能够导向全局最优解。所以并不是所有的问题都能得到全局最优解。

         典型的例子如分数背包问题:背包容量为50kg,有三个商品分别是重60元/10kg、100元/20kg、120元/30kg,商品可以分割,如何选取商品使背包价值最大。这里使用贪心算法,应当优先选择单位重量价值最大的商品,三种商品的单位重量价值分别为6、5、4,所以分别选取10kg、20kg和20kg,总价值为240元。但是,对于0-1背包问题——商品不能分割,此时的最佳方案是商品二20kg、商品三30kg总价值220元而不选择原本单位重量价值最高的商品一。由于背包的多余空间会改变商品的单位重量价值,所以不能以贪心算法来求解,应当转用动态规划。

         贪心算法最关键的一点在于要证明每一次做出的局部最优解与剩下子问题的最优解组合可以得到原问题的全局最优解

活动选择问题

下表为多个活动的开始时间si与结束时间fi,同一时间只能有一个活动进行,要找出一个不冲突的最大兼容活动集合。

i

1

2

3

4

5

6

7

8

9

10

11

si

1

3

0

5

3

5

6

8

8

2

12

fi

4

5

6

7

9

9

10

11

12

14

16

对于这个例子{a1,a4,a8,a11}和{a2,a4,a9,a11}都是最大兼容活动子集。

如果我们使用贪心算法,可以想到的策略应该是尽可能选择结束早的活动,给剩余活动留下更大的选择范围。

该想法成立由一个前提:任意非空子问题Sk,令am是Sk中结束时间最早的活动,则am在Sk的某个最大兼容活动子集中

证明Ak是Sk的一个最大兼容活动子集,且aj是Ak中结束时间最早的活动。若aj=am,则已经证明am在Sk的某个最大兼容活动子集中。若aj≠am,令集合Ak'=Ak-{aj}∪{am},即将Ak中的aj替换为am。Ak'中的活动都是不相交的,Ak中的活动也都不是不相交的,aj是Ak中结束时间最早的活动,而fm≤fi。由于丨Ak'丨=丨Ak丨,因此得出结论Ak'也是Sk的一个最大兼容活动子集且包含am

因此贪心算法是成立的。代码实现上,可以使用递归法:对于原问题选出最早结束的任务与剩余部分的最大兼容子集。后者可以继续递归下去。

1 resursiveActivitySelector(s,f,k,n){//初始调用参数为(s,f,0,n)
2     m=k+1;
3     for(m=k+1;m<=n && s[m]<f[k];m++);//寻找结束时间大于f[k]的最早结束的任务
4     if(m<=n)
5         return {a[m]} ∪ resursiveActivitySelector(s,f,k,n);
6     else
7         return NULL;
8 }

这属于一个典型的尾递归,也可以使用迭代算法,只需要不断寻找当前剩余活动中最早结束的那个加入到结果集合中。

greddyActivitySelector(s,f){
    n=s.length;
    A={a[1]};
    k=1;
    for(m=2;m<=n;m++){
        if(s[m]>=f[k]){
            A.add(a[m]);
            k=m;
        }
    }
    return A;
}

霍夫曼编码

         霍夫曼编码是通过将不同出现频率的字符编码为变长编码,出现频率高的则编码短,以节省空间。现在有一个文件中仅有6个不同字符出现,频率如下表所示

 

a

b

c

d

e

f

频率(千次)

45

13

12

16

9

5

定长编码

000

001

010

011

100

101

变长编码

0

101

100

111

1101

1100

         可以看到变长编码实际需要空间是(45*1+13*3*12*3+16*3+9*4+5*4)*1000=224,000位,而定长编码是300,000位。使用变长编码要求没有任何码字是其他码字的前缀,即所有编码可以唯一对应到一个结果。

         霍夫曼编码就是通过贪心算法来构造最优前缀码:

首先将所有字符按频率升序排列: f5 e9 c12 b13 d16 a45。从中取出最小两者相加f:5+e:9=14,重新按照大小插入回队列中: 12 13 14 16 45。然后再取出最小两者相加并插入:14 16 25 45。后续为25 30 45;45 55;100。对于每次取出的两个点,将他们作为二叉树的两个节点,他们的和为父节点,最终形成一颗二叉树,然后对每个分叉标上0和1,路径就是每个节点的变长编码。由于霍夫曼编码每次要获取两个最小值的特点,可以采用小堆排序来实现。

 

posted @ 2018-07-31 17:24  GrayWind  阅读(291)  评论(0编辑  收藏  举报