贪心算法在竞赛中的应用

来源:互联网 发布:手机桌面提醒软件 编辑:程序博客网 时间:2024/05/01 13:30
原著:Hal Burch 翻译:怒火之袍
译者的话:

    Hal Burch在1999年春天通过分析得出了一个惊人的发现,实际上只存在16种竞赛试题类型。而在IOI中,前几种就构成了约80%的问题 。贪心算法(又译作贪婪算法)就是这“前几种”之一。

    当一个问题具有最优子结构时,我们会想到用动态规划法去解它,但是有些问题存在着更简单有效的方法,我们只要总是做出当前看来最好的选择就可以了。贪心算法所作的选择可以依赖于以往所作过的选择,但决不依赖于将来的选择,也不依赖于子问题的解,这使得算法在编码和执行的过程中都有着一定的速度优势。如果一个问题可以同时用几种方法解决,贪心算法应该是最好的选择之一。

    借助“拟阵”的工具,我们可以建立一个关于贪心算法的较一般的理论,但在竞赛这种场合中,更多需要的是个人的经验来判断何时该使用贪心算法。因为贪心算法并不是对所有的问题都能得到整体最优解或最理想的近似解,与回溯法等通法比较,它的适用区域相对狭窄许多,因此正确的判断它的应用时机十分重要。总之,贪心算法并不深奥,却可以解决许多看似深奥的问题(比如刚刚过逝的Dijkstra对它在单源最短路径问题中的应用),译者所翻译的这篇文章就是一个外国选手在这方面的一些经验,应该说他谈的并不是十分深入,但是他向我们很好的展示了怎样用通俗的思维分析贪心算法的主要问题、正确性、使用时机和技巧,我想对于各个水平的读者都会有一定程度的帮助。


贪心算法

    一、问题举例:谷仓修补[1999 USACO Spring Open]

    有一长列畜栏,其中的一些需要用木板覆盖。你可以用最多N个(1<=N<=50)木板,其中的每一个都可以覆盖任意数量的连续畜栏。覆盖全部需要覆盖的畜栏,但是使被覆盖的畜栏尽量少。

    思想:
    

    在贪心算法背后隐藏的基本思想是从小的方案推广到大的解决方法。然而与其他方法不同的是,贪心算法只需随着过程的进行保持现下的最好方案。因此,对于这个例题,如果需要找到N=5时的最优方案,应该寻找N=4时的最优解,然后加以改变得到N=5的解法。至于N=4时的其它解法,可以不予考虑。

    贪心算法快速,而且仅需要很小的额外内存消耗。但是很不幸地,它往往是不正确的。然而当他们的确是正确的时候,便可以很轻易地贯彻执行并拥有足够快的速度。

    问题:
    

    贪心算法有两个基本的问题。

    如何建立:


    怎样从一个规模较小的解推出规模较大的解呢?拿这个例题来说,从四个木板变化到五个木板的最明显的途径是将一个木板移去一部分而拆成两个。你应该选择移除最大的只覆盖了不需要覆盖的部分。
为了移除这样的部分,选择跨越了这部分的一块木板将它拆成两部分,一块覆盖这些畜栏之前的部分,另一块覆盖这些畜栏之后的部分。

    解法确实正确?


    对于程序员来说,真正的挑战在于贪心算法不一定总是有效这个事实。即使它对于特定输入,随机输入,乃至一切你能想到的情况都是正确的,但是如果如果有一种情况它不能正确地工作,至少一个(或者更多)的裁判测试就会是这种类型。

    对于这个例题,为了确保贪心算法确实是有效的应该做如下的考虑:

    假设答案并没有包含贪心算法移除的大型缺口,而是包含了一个小一些的缺口,通过将较小缺口两端的木板合并并炸开跨越了更大缺口的木板所得到的答案,使用了和原先一样多的木板,但是覆盖的畜栏更少一些。这个新的方案更好一些,所以之前的假设是错误的,我们总是应该选择移除最大的缺口。

    如果答案没有包含这个特定的缺口而是包含了一个正好和它一样大的缺口,通过同样做法所产生出的答案在覆盖的畜栏和使用的木板数上均与原来相同。新方法与原方法效果相同而不是更加出色,所以我们选择哪一个都是可以的。

    因此,确实存在着一个包含了大缺口的最佳答案,每步都是如此,每前进一步所得到的最优方案都是上一步的超集。因此最后的方案是最优的。

    结论:


    如果一个贪心解决方案存在,就使用它。它易于编码,易于调试,运行快速,消耗内存少,是竞赛中的很好的算法。这其中唯一缺少的成分是正确性。如果贪心算法找到了正确的答案,坚决地使用它,但是永远不要以为贪心算法对全部的问题都有效。

    二、问题举例:对一个有三个值的序列排序[IOI 1996]
    

    有一个包含三个值(1,2,3)的序列,其长度不超过1000。寻找一个方案使用最小次数的交换将序列排序。

    排序后的序列被分为三个部分,为1的部分,为2的部分和为3的部分。贪心算法将尽可能多的1部分中的2与2部分中的1交换,将尽可能多的1部分中的3与3部分中的1交换,将尽可能多的2部分中的3与3部分中的2交换。一旦这种情况不再存在,剩余的不在它应该在的位置上的元素需要三个三个的旋转。使所有的1落位,然后使所有的2落位即可。

    分析:

    明显地,一次交换能够最多使两个元素位于他们应该所处的位置,所以所有第一种的交换都是最优的。同时,它们使用的是不同类型的元素,所以在这些种类之间不存在冲突。这意味着顺序是无关紧要的。一旦这些交换被执行之后,你能做的最好的选择则是通过两次交换使三个不在它们位置上的元素落位,这就是第二部分工作所要完成的任务。(举例来说,假如所有的1都已经在它们应该在的位置上,但是还有一些2在3部分中,则必然有同样的3在2部分中,这时已经可以进行直接的交换了。)

    三、一个反例:友好的硬币(经删节)

    有一个新近建立的国家叫做the Dairy Republic,你将获得的信息包括这个国家的硬币面值信息和一些金钱数量。试用最小数目的硬币凑出这个数量的金钱。the Dairy Republic肯定含有1分的硬币。(译者注:这是一个保证所有情况下一定数量的钱都能被拼凑出来的充分非必要条件)

    错误的分析:

    显然地,你永远不会考虑去选择一个较小面值的硬币,因为那意味着你必须拿走更多数量的硬币,所以贪心算法是可行的。

    也许不是这样:OK,这个算法通常是有效的。实际上,对于美国的硬币体系{1,5,10,25},它总能产生出最优的解法。然而对于其他体系,像是{1,5,8,10},如果目标是13,使用贪心算法将拿走一个10和三个1,总共四枚硬币。但是只包含两个硬币的解{5,8}同样是存在的。

    四、拓扑排序
    

    现在给定一群对象,其中包含了一些顺序上的约束条件,比如“A 必须排在B的前面”,寻找一种顺序使得所有的约束条件都能成立。

    算法:
    

    通过这些对象创造一个图,如果有“A 必须排在B的前面”,则使从A到B之间有一条弧。用任意的顺序制造一个穿越这些对象的路径。每当你找到一个入度(内分支度)为0的对象,就贪婪地将它放在现有顺序的最后,删除所有以它为起点的弧,然后对排在它之前的儿子对象做同样的检查。如果这个算法没有使所有的对象都经过这样的排序就通过了所有的对象,则没有一个顺序满足这些约束条件。

原创粉丝点击