在解决一些问题的时候,有的问题要得到最优解是非常困难的。这个时候我们就不要再执着于最优解了,因为即便得到了最优解也可能是得不偿失的。采用贪心算法求解这个问题是一种折中的好方法。

所谓贪心算法就是在对问题求解时,总是做出当前看来是最好的选择。也就是说贪心算法并不从整体最优上考虑问题,算法得到的是某种意义上的局部最优解。而局部的最优解叠加在一起便构成了问题的整体最优解,或者近似最优解。正是因为贪心算法思想简单,且算法效率较高,所以在一些问题的解决上有着明显的优势。

下面我们来看一个经典的问题——集合覆盖问题,从中体会一下贪心算法的妙用。

假设你办了个广播节目,要让全美 50个州的听众都收听得到。为此,你需要决定在哪些广播台播出。在每个广播台播出都需要支付费用,因此你力图在尽可能少的广播台播出。现有广播台名单(部分)如下。

表(1)每个广播台和对应覆盖的州

广播台名称 广播台

覆盖的州

(1) KONE

ID,NV,UT

(2) KTWO

WA,ID,MT

(3) KTHREE

OR,NV,CA

(4) KFOUR

NV,UT

(5) KFIVE

CA,ZA

如何找出覆盖全美 50个州的最小广播台集合呢?

一般情况下,我们会想到使用穷举法来解决这个问题。假设全美共有n个广播台可供选择,那么每个广播台就有“选择”(1)和“不选择”(0)两种状态,将这n个广播台中每个广播台的两种选择方式任意组合,则共有2^n种组合方式,也就是共有2^n个可能的广播台集合。现在我们要做的就是从这2^n个集合中找出一个集合,要求该集合中的广播台可以覆盖全美50个州,同时广播台的数量是最小的。

这个方法看似简单直观,但是却非常耗时,因为该方法的时间复杂度达到了O(2^n)。如果广播台的数量不够多(例如表1中所示仅有5个),则穷举法依然是可行的,毕竟可以在有限时间内找到问题的最优解;但是如果广播台的数量很多(例如题目中要求的50个),则消耗的时间会非常长,因为随着广播台的增多,消耗的时间将呈指数(2^n)级增长!假设每秒钟可得到10个广播台的集合,随着广播台数量的增加,所需时间变化如表2所示

表(2)广播台的数量和计算这些数量下广播台组合所需时间

广播台数量

计算该数量下广播台组合所需时间

5

3.2s

10

102.4s

32

13.6年

100

4×10^23年

可见如果广播台数量较大,穷举法不是一个可行的解决方案。

于是我们就要祭出终极大招了——贪心算法登场!

因为我们既要让选择的广播台可以覆盖全美50州,同时又要广播台的数量尽可能少,所以我们可在选择广播台时优先选择可覆盖最多未覆盖州的广播台,即便这个广播台覆盖了一些已经被覆盖的州也无妨。这样就可以以更快的速度将全部的州覆盖,从而实现广播台数量尽可能少的目标。

这么说可能有些拗口,其实很好理解,我们通过一个简单的例子来理解该贪心算法的精髓。

假设现在只有9个州(A,B,C,D,E,F,G,H,I)和5个广播台(1,2,3,4,5),广播台对各州的覆盖情况如图(1)所示。

图(1)广播台覆盖各州的情况

如图(1)所示,格广播台对各州的覆盖关系如下:

  1. --> A、B、D、E
  2. -->D、E、G、H
  3. -->G、H、I
  4. -->C、F、I
  5. -->B、C

现在要找出尽可能少的广播台,同时又要全部覆盖这9个州,我们可以使用贪心策略来解决这个问题。

1、最初9个州无一被覆盖,所以未覆盖的州为{A,B,C,D,E,F,G}

2、然后选择可覆盖最多未覆盖州的广播台。因为广播台(1)(2)均可覆盖4个州,广播台(3)(4)均可覆盖3个州,广播台(5)可覆盖2个州,所以选择(1)(2)都是可以的,这里选择广播台(1)。于是未覆盖的州变为{C,F,I,G,H},如图(2)所示。

图(2) 选择广播台(1)后覆盖了A、B、D、E四个州

3、接下来要选择能覆盖{C,F,I,G,H}中最多州的那个广播台。我们可通过计算交集的办法来找出这个广播台。广播台(2)可覆盖的州为{D,E,G,H},而集合{D,E,G,H}与{C,F,I,G,H}的交集为{G,H},这说明广播台(2)仅可覆盖未被覆盖的州中的2个州G和H。按照同样的方法计算,广播台(3)可覆盖的未覆盖的州为{G,H,I}×{C,F,I,G,H}={G,H,I};广播台(4)可覆盖的未覆盖的州为{C,F,I}×{C,F,I,G,H}={C,F,I};广播台(5)可覆盖的未覆盖的州为{C,F,I,G,H}×{B,C}={C}。所以可选择广播台(3)或广播台(4),它们都可以覆盖3个未被覆盖的州。这里选择广播台(3),于是未覆盖的州变为{C,F},如图(3)所示。

图(3) 选择广播台(3)后覆盖了G、H、I三个州

4、接下来要选择能覆盖{C,F}中最多州的那个广播台。我们同样用计算交集的方法来找出这个广播台。此时还剩下广播台(2)(4)(5),我们分别计算它可覆盖州的集合与未覆盖州的集合{C,F}的交集。广播台(2)可覆盖的未覆盖的州为{D,E,G,H}×{C,F}=null;广播台(4)可覆盖的未覆盖的州为{C,F,I}×{C,F}={C,F};广播台(5)可覆盖的未覆盖的州为{B,C}×{C,F}={C}。显然广播台(4)可覆盖最多未覆盖的州,因此选择广播台(4)。如图(4)所示。

图(4) 选择广播台(4)后覆盖了C、F、I三个州

至此全部的9个州都被广播台(1)(3)(4)覆盖。

在上述计算过程中,利用贪心策略逐步找出最优的广播台组合,从而既实现了9个州全覆盖,又选择了最少的州数。可以看出,在使用贪心算法解决问题时并不从问题的整体最优解出发,而是只“贪心”地着眼于当下,只要每次选取的广播台可以最大限度地覆盖未覆盖的州即可。这种方法要比上面介绍的逐一穷举可能解,在从中找出最优解的方法高效很多。贪心算法的时间复杂度为O(n^2),其中n为广播台的个数。这就是贪心算法的优势所在。

另外,使用贪心算法求解问题得到的答案可能并不唯一。例如本题,如果最开始我们选择的不是广播台(1)而是广播台(2),那么使用该贪心策略得到的结果就是(2)(4)(1),这个结果同样可以覆盖9个州。如图(5)所示。

图(5) 最开始选择广播台(2)最终得到的结果

下面给出本题完整的代码。

public class SetCover {  public static HashSet<String> getBestBroadCasts(HashSet<String> allStatesSet, LinkedHashMap<String, HashSet<String>> broadCasts ) {HashSet<String> bestbroadCasts = new  HashSet<String>();String bestBroadCast = "";//外层循环控制将所有州allStatesSet全部覆盖掉while (allStatesSet.size()>0) {//内层循环遍历每一个广播站,得到其对应覆盖的州//然后计算这些州与剩余未覆盖州的交集//选出其中可覆盖最多未覆盖州对应的那个广播站,将其放到bestbroadCasts集合里HashSet<String> maxCovered = new HashSet<String>();for (HashMap.Entry<String, HashSet<String>> map: broadCasts.entrySet()) {HashSet<String> set = map.getValue();  //得到该广播站可覆盖的州的集合       //计算该广播站可覆盖的州与剩余未覆盖州的交集HashSet<String> covered = new HashSet<String>();covered.clear();covered.addAll(set);covered.retainAll(allStatesSet);//for循环结束后,maxCovered中保存可覆盖的最多未覆盖的州//bestBroadCast中保存对应的广播站的名字if (covered.size() > maxCovered.size()) {maxCovered = covered;bestBroadCast = map.getKey();//System.out.println("covered " + covered);System.out.println("maxCovered " + maxCovered);System.out.println("bestBroadCast " + bestBroadCast);}}bestbroadCasts.add(bestBroadCast);  //将bestBroadCast加入bestbroadCasts//计算allStatesSet与maxCovered的差集,这样allStatesSet会缩小,将剩下尚未被覆盖的州allStatesSet.removeAll(maxCovered);              } return bestbroadCasts;}public static void main(String[] args) {
//初始化allStates,存放所有需要覆盖的州String[] allStates = {"mt", "wa", "or", "id", "nv", "ut","ca","az"};
//将字符串数组转换为集合HashSet
HashSet<String> allStatesSet = new HashSet<String>(Arrays.asList(allStates)); //创建一个HashMap broadCasts,存放广播台和每个广播台可覆盖的州LinkedHashMap<String, HashSet<String>> broadCasts
= new LinkedHashMap<String, HashSet<String>>();//初始化broadCastsbroadCasts.put("kone",new HashSet<String>(Arrays.asList("id", "nv","ut")));broadCasts.put("ktwo",new HashSet<String>(Arrays.asList("wa", "id","mt")));broadCasts.put("kthree",new HashSet<String>(Arrays.asList("or", "nv","ca")));broadCasts.put("kfour",new HashSet<String>(Arrays.asList("nv","ut")));broadCasts.put("kfive",new HashSet<String>(Arrays.asList("ca","az")));//调用getBestBroadCasts函数得到广播台信息System.out.println(getBestBroadCasts(allStatesSet,broadCasts));}
}

为了简化问题的规模,上述代码仅实现了表1所示的5个广播台和所覆盖的8个州的情形。而且在本代码中多采用HashSet,HashMap结构存放数据,这样做的目的是为了充分利用Java容器类的方法,使操作更加方便。

在main方法中首先定义了一个字符串数组allStates用来存放8个州的名字,然后将该字符串数组转换成为一个HashSet集合allStatesSet。然后创建了一个HashMap对象broadCasts,用来存放每个广播台以及该广播台对应的所能覆盖的州的信息。例如广播台”kone”可覆盖的州为"id", "nv","ut",那么就将字符串”kone”作为broadCasts中一项的key值,将由"id", "nv","ut"组成的HashSet作为该key对应的value。最后调用函数getBestBroadCasts()计算最优的广播台选择方案。

函数HashSet<String> getBestBroadCasts(HashSet<String> allStatesSet, LinkedHashMap<String, HashSet<String>> broadCasts ) 的作用就是利用贪心算法计算算最优的广播台选择方案。该函数的返回值为一个HashSet<String>的对象,里面将保存选取的广播台的名字。该函数包含两个参数,即为前面讲到的allStatesSet和broadCasts。

在函数getBestBroadCasts()内部通过一个二重循环实现贪心法查找最优广播台组合。外层循环的执行条件是allStatesSet.size()>0,也就是说只要allStatesSet中存储的州还没有被选中的广播台全部覆盖,就继续执行下去。因此外层的while循环控制将所有州allStatesSet全部覆盖掉。内层循环将遍历Hashmap broadCasts中保存的每一个广播站,并得到其对应覆盖的州,然后计算这些州与剩余的未覆盖州的交集,从中选出可覆盖最多未覆盖州对应的那个广播站,并将其放到bestbroadCasts集合里。当一次内层循环执行完毕后,就可以选出一个广播台,该广播台可覆盖最多的未覆盖州。每当执行完一次内层for循环后,还要执行allStatesSet.removeAll(maxCovered);目的是为了将已覆盖的州从集合allStatesSet中移除,这样allStatesSet中始终保存的是尚未覆盖的州。当allStatesSet.size()==0时表明所有的州都已被选中的广播台覆盖,所以外层循环就可以结束。

最终集合bestbroadCasts中将会保存选中的广播台信息,并作为返回值返回给主调函数。

上述代码的执行结果如图(6)所示

图(6)程序的执行结果

需要指出的是,由于贪心算法每一步都只考虑局部的最优解,所以在处理某些问题上可能得不到整体的最优解。从严格意义上讲,要使用贪心算法得到最优解,该问题应具备以下性质。

  1. 贪心选择性质

所谓贪心选择性质就是指所求解问题的整体最优解可以通过一系列局部最优解得到。例如上述的“找钱问题”中,当前状态下最优的选择就是使找过硬币后剩余的零钱数(还亏欠顾客的零钱数)最接近于0,所以每次找钱都要选择面值尽可能大的硬币,这样硬币的总数才会更少。

  1. 最优子结构性质

当一个问题的最优解包含着它的子问题的最优解时,则称该问题具有最优子结构性质。上述“找钱问题”就是典型的具有最优子结构性质。

在实践中,要判断一个问题是否具备以上两种性质则需要严格的数学证明。因此虽然贪心算法简单且易于实现,但是理论上在使用贪心算法解决问题之前,要对问题本身进行深入透彻的分析和证明,以确保使用贪心算法可以得到整体最优解。

        然而经验告诉我们,实际应用中许多问题都可以使用贪心算法得到最优解,即使贪心算法得不到问题的最优解,最终的结果也是最优解的很好的近似解。所以在解决一般性问题时,我们可以大胆尝试使用贪心算法。

我们熟悉的哈夫曼编码算法,以及前面章节中介绍的最小生成树的Prim算法和Kruskal算法,以及计算图的单源最短路径的Dijkstra算法等都是基于贪心算法的思想设计出来的算法。

        想要算法面试类精选文章,可关注我的微信公众号 @算法匠人

最高效的近似算法——贪心算法相关推荐

  1. 算法基础(Java)--贪心算法

    前言 前面简单的介绍了八大经典排序算法,此文将要介绍贪心算法,并介绍一些常见贪心算法题目. 1. 贪心算法的概念 所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择.也就是说,不从整体最 ...

  2. 贪心算法简单实践 -- 分糖果、钱币找零、最多区间覆盖、哈夫曼编解码

    1. 贪心算法概览 贪心算法是一种算法思想.希望能够满足限制的情况下将期望值最大化.比如:Huffman编码,Dijkstra单源最短路径问题,Kruskal最小生成树 等问题都希望满足限制的情况下用 ...

  3. Runnable接口和贪心算法

    1 Runnable接口 Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现.设计该接口的目的是为希望在活动时执行代码的对象提供一个公共协议.激活的意思是说某个线程已启动并且尚未停止 ...

  4. 总结 贪心算法_这几道经典例题帮你轻松搞透贪心算法

    贪心算法概念叙述 运用贪心算法求解问题时,会将问题分为若干个子问题,可以将其想象成俄罗斯套娃,利用贪心的原则从内向外依次求出当前子问题的最优解,也就是该算法不会直接从整体考虑问题,而是想要达到局部最优 ...

  5. 五大常用算法之三:贪心算法

    一.基本概念: 所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择.也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解. 贪心算法没有固定的算法框架,算法设计的关键 ...

  6. “365算法每日学计划”:03打卡-贪心算法

    自从开始做公众号开始,就一直在思考,怎么把算法的训练做好,因为思海同学在算法这方面的掌握确实还不够.因此,我现在想做一个"365算法每日学计划". "计划"的主 ...

  7. 哈夫曼编码压缩率计算_程序员的算法课(8)-贪心算法:理解霍夫曼编码

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/m0_37609579/article/ ...

  8. 活动安排问题--贪心算法

    活动安排问题就是要在所给的活动集合中选出最大的相容活动子集合,是可以用贪心算法有效求解的很好例子.该问题要求高效地安排一系列争用某一公共资源的活动.贪心算法提供了一个简单.漂亮的方法使得尽可能多的活动 ...

  9. 嵌入式必会!C语言最常用的贪心算法就这么被攻略了

    01 基本概念 贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择.也就是说,不从整体最优上加以考虑,只做出在某种意义上的局部最优解.贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的 ...

最新文章

  1. 或许有一两点你不知的C语言特性
  2. PB控制性能TreeView
  3. 在BackTrack下基于MS08-067漏洞的渗透
  4. python word2vec 移植_真实资讯语料下的Word2Vec的迁移实践:Tag2Vec
  5. 诺基亚困境,力掐苹果?
  6. 3D计算机组装,3DMax电脑组装配置
  7. windows 匿名管道: 父进程与子进程通信 (进程间通信之CreatePipe)
  8. (安徽省地图)行政区划图高清矢量cdr|pdf(详细版2021年)
  9. python学习之函数(一)
  10. hyperledger fabric 2.3.3 搭建教程
  11. 计算机机房门尺寸,门的宽度是多少
  12. android 区分平板,加量不加价!台电首款基于Android 11的平板终上市
  13. WORD2003相关问题
  14. 小成开发日记-----利用Python在互动吧网站自动抢票实战分析
  15. 【2】基因功能注释之SwissProt和Interproscan
  16. comsol移动网格_[转载]COMSOL动网格调试
  17. 自媒体创作的注意事项,一篇好的文章需要具备哪些要素?
  18. 西电毕业论文Latex排版教程
  19. 前端构建工具-fis3使用入门
  20. java综合实训报告_java综合实验报告1.doc

热门文章

  1. asio非boos版本使用
  2. Exchange Online手工迁移方案 | 分享二
  3. 华为OD机试 - 九宫格(Java JS Python)
  4. 交换机开启SSH远程登录
  5. 货款监管存疑 电商开放平台难逃自建支付
  6. 货款违约预测第二周周报
  7. 假如从来没有开始,你怎么知道自己会不会很爱很爱那个人呢
  8. 【图像压缩】QOI图像格式详解
  9. 晶振虚焊导致TI 28335 DSP 烧写FLASH后,连接仿真器时正常工作,拔掉仿真器却不能启动运行...
  10. Dicom中的Image Orientation/Position的理解和应用