Sample Problem: Barn Repair [1999 USACO Spring Open]
There is a long list of stalls, some of which need to be covered with boards. You can use up to N (1 <= N <= 50) boards, each of which may cover any number of consecutive stalls. Cover all the necessary stalls, while covering as few total stalls as possible.
The Idea
The basic idea behind greedy algorithms is to build large solutions up from smaller ones. Unlike other approaches, however, greedy algorithms keep only the best solution they find as they go along. Thus, for the sample problem, to build the answer for N = 5, they find the best solution for N = 4, and then alter it to get a solution for N = 5. No other solution for N = 4 is ever considered.
Greedy algorithms are fast, generally linear to quadratic and require little extra memory. Unfortunately, they usually aren't correct. But when they do work, they are often easy to implement and fast enough to execute.
Problems
There are two basic problems to greedy algorithms.
How to Build
How does one create larger solutions from smaller ones? In general, this is a function of the problem. For the sample problem, the most obvious way to go from four boards to five boards is to pick a board and remove a section, thus creating two boards from one. You should choose to remove the largest section from any board which covers only stalls which don't need covering (so as to minimize the total number of stalls covered).
To remove a section of covered stalls, take the board which spans those stalls, and make into two boards: one of which covers the stalls before the section, one of which covers the stalls after the section.
Does it work?
The real challenge for the programmer lies in the fact that greedy solutions don't always work. Even if they seem to work for the sample input, random input, and all the cases you can think of, if there's a case where it won't work, at least one (if not more!) of the judges' test cases will be of that form.
For the sample problem, to see that the greedy algorithm described above works, consider the following:
Assume that the answer doesn't contain the large gap which the algorithm removed, but does contain a gap which is smaller. By combining the two boards at the end of the smaller gap and splitting the board across the larger gap, an answer is obtained which uses as many boards as the original solution but which covers fewer stalls. This new answer is better, so therefore the assumption is wrong and we should always choose to remove the largest gap.
If the answer doesn't contain this particular gap but does contain another gap which is just as large, doing the same transformation yields an answer which uses as many boards and covers as many stalls as the other answer. This new answer is just as good as the original solution but no better, so we may choose either.
Thus, there exists an optimal answer which contains the large gap, so at each step, there is always an optimal answer which is a superset of the current state. Thus, the final answer is optimal.
Conclusions
If a greedy solution exists, use it. They are easy to code, easy to debug, run quickly, and use little memory, basically defining a good algorithm in contest terms. The only missing element from that list is correctness. If the greedy algorithm finds the correct answer, go for it, but don't get suckered into thinking the greedy solution will work for all problems.
Sample Problems
Sorting a three-valued sequence [IOI 1996]
You are given a three-valued (1, 2, or 3) sequence of length up to 1000. Find a minimum set of exchanges to put the sequence in sorted order.
Algorithm The sequence has three parts: the part which will be 1 when in sorted order, 2 when in sorted order, and 3 when in sorted order. The greedy algorithm swaps as many as possible of the 1's in the 2 part with 2's in the 1 part, as many as possible 1's in the 3 part with 3's in the 1 part, and 2's in the 3 part with 3's in the 2 part. Once none of these types remains, the remaining elements out of place need to be rotated one way or the other in sets of 3. You can optimally sort these by swapping all the 1's into place and then all the 2's into place.
Analysis: Obviously, a swap can put at most two elements in place, so all the swaps of the first type are optimal. Also, it is clear that they use different types of elements, so there is no ``interference'' between those types. This means the order does not matter. Once those swaps have been performed, the best you can do is two swaps for every three elements not in the correct location, which is what the second part will achieve (for example, all the 1's are put in place but no others; then all that remains are 2's in the 3's place and vice-versa, and which can be swapped).
Friendly Coins - A Counterexample [abridged]
Given the denominations of coins for a newly founded country, the Dairy Republic, and some monetary amount, find the smallest set of coins that sums to that amount. The Dairy Republic is guaranteed to have a 1 cent coin.
Algorithm: Take the largest coin value that isn't more than the goal and iterate on the total minus this value.
(Faulty) Analysis: Obviously, you'd never want to take a smaller coin value, as that would mean you'd have to take more coins to make up the difference, so this algorithm works.
Maybe not: Okay, the algorithm usually works. In fact, for the U.S. coin system {1, 5, 10, 25}, it always yields the optimal set. However, for other sets, like {1, 5, 8, 10} and a goal of 13, this greedy algorithm would take one 10, and then three 1's, for a total of four coins, when the two coin solution {5, 8} also exists.
Topological Sort
Given a collection of objects, along with some ordering constraints, such as "A must be before B," find an order of the objects such that all the ordering constraints hold.
Algorithm: Create a directed graph over the objects, where there is an arc from A to B if "A must be before B." Make a pass through the objects in arbitrary order. Each time you find an object with in-degree of 0, greedily place it on the end of the current ordering, delete all of its out-arcs, and recurse on its (former) children, performing the same check. If this algorithm gets through all the objects without putting every object in the ordering, there is no ordering which satisfies the constraints.
示例问题:谷仓修复[1999 USACO春季开放]
有很多摊位,其中一些需要用木板覆盖。您最多可以使用N个(1 <= N <= 50)个板,每个板可以覆盖任意数量的连续停顿。覆盖所有必要的摊位,同时覆盖尽可能少的摊位。
理念
贪婪算法背后的基本思想是从较小的算法构建大型解决方案。然而,与其他方法不同,贪心算法只保留了他们发现的最佳解决方案。因此,对于样本问题,为了建立N = 5的答案,他们找到N = 4的最佳解,然后改变它得到N = 5的解。对于N = 4没有其他解决方案被考虑过。
贪婪算法速度很快,一般为线性二次方式,只需要很少的额外内存。不幸的是,他们通常不正确。但是,当他们工作时,他们通常很容易实施并且足够快地执行。
问题
贪婪算法有两个基本问题。
如何建立
如何从较小的解决方案创建更大的解决方案?一般来说,这是问题的一个功能。对于样本问题,从四块板到五块板最明显的方法是选择一块板并移除一块,从而从一块板上创建两块板。您应该选择从任何只覆盖不需要覆盖的摊位的板上移除最大的部分(以便最小化所覆盖的摊位总数)。
要拆除一部分有遮盖的摊位,拿走跨过这些摊位的板子,并制成两块板子:其中一块覆盖在该部分之前的摊位,其中一块覆盖该部分之后的摊位。
它工作吗?
程序员面临的真正挑战在于贪婪的解决方案并不总是能够工作。即使它们似乎适用于样本输入,随机输入以及您可以想到的所有情况,如果存在无法工作的情况,至少有一个(如果不是更多!)评委的测试用例将会是那种形式。
对于示例问题,要查看上述贪婪算法的工作原理,请考虑以下事项:
假设答案不包含算法删除的大间隙,但确实包含较小的间隙。通过将两块电路板组合在较小的间隙末端并将电路板分割成较大的间隙,就可以得到一个答案,它使用与原始解决方案一样多的电路板,但占用的电路板数量更少。这个新答案更好,因此假设是错误的,我们应该总是选择消除最大的差距。
如果答案不包含这个特殊的差距,但确实包含另一个同样大的差距,那么进行相同的转换会产生一个答案,它使用尽可能多的电路板并覆盖与另一个答案一样多的电阻。这个新答案与原始解决方案一样好,但没有更好,所以我们也可以选择。
因此,存在一个包含较大差距的最佳答案,所以在每一步中总会有一个最佳答案,它是当前状态的一个超集。因此,最终的答案是最佳的。
结论
如果存在贪婪的解决方案,请使用它。它们易于编码,易于调试,运行速度快,占用内存少,基本上可以在竞赛中定义一个好的算法。该列表中唯一缺少的元素是正确性。如果贪婪算法找到了正确的答案,那就去做吧,但不要被认为贪婪的解决方案会适用于所有问题。
示例问题
排序三值序列[IOI 1996]
您将获得一个长度为1000的三值(1,2或3)序列。找到一组最小的交换,以便按顺序排列序列。
算法 序列有三个部分:按排序顺序为1,按排序顺序为2,按排序顺序为3。贪婪算法尽可能多地交换了2部分中的1与2中的1部分,尽可能多地1中的3部分中的3中的1部分中,以及3中的2与3中的2部分。一旦这些类型都不存在,剩余的元素需要以3个为一组的方式旋转。您可以通过将所有1个交换到位然后将所有2个放到位来对它们进行最佳排序。
分析:显然,交换最多可以放置两个元素,因此第一类交换的所有交换都是最优的。另外,显然他们使用不同类型的元素,所以这些类型之间不存在“干扰”。这意味着订单无关紧要。一旦这些掉期交易完成后,你可以做的最好的做法是每隔三个元素进行两次交换,而不是在正确的位置,这是第二部分将实现的目标(例如,所有1都放在适当位置,但没有其他位置;然后全部剩下的是3的位置2,反之亦然,可以交换)。
友好硬币 - 反例[删节]
鉴于新成立的国家,乳品共和国和一些货币金额的硬币面额,找到总和达到该金额的最小硬币集。乳品共和国保证有1美分的硬币。
算法:取不超过目标的最大硬币值并迭代总数减去此值。
(故障)分析:显然,你永远不想拿一个较小的硬币价值,因为这意味着你必须拿出更多的硬币来弥补差异,所以这个算法有效。
也许不是:好的,算法通常有效。实际上,对于美国硬币系统{1,5,10,25},它总是产生最佳集合。然而,对于其他集合,如{1,5,8,10}和13的目标,这个贪婪算法将花费一个10,然后三个1,总共四个硬币,当两个硬币解{ 8}也存在。
拓扑排序
给定一组对象,以及一些排序约束,例如“A必须在B之前”,找到对象的顺序,使得所有排序约束成立。
算法:在对象上创建有向图,如果“A必须在B之前”,则存在从A到B的弧。以任意顺序传递对象。每次找到一个度数为0的对象时,贪婪地将它放在当前顺序的末尾,删除它的所有out-arcs,并对它的(以前的)子进行递归,执行相同的检查。如果该算法通过所有对象而不将每个对象放在排序中,则没有满足约束的排序。