在以前的5篇博客中,咱们学习了动态规划算法。咱们能够看到,在求解最优化问题的算法中,一般须要通过一系列的步骤,在每一个步骤中都面临多种选择。对于许多最优化问题,使用动态规划算法来求解最优解有些杀鸡用牛了,可使用更加简单的算法。贪心算法(greedy algorithm)就是其中之一:它在每一步作出的选择都是当时看起来的最优选择。也便是说,它老是作出局部最优选择,以此但愿这样的选择可以产生全局的最优解。python
咱们先来看看一个适应贪心算法求解的问题:选择活动问题。算法
假定有一个\(n\)个活动的集合\(S = \{ a_1, a_2,...,a_n \}\),每一个活动\(a_i\)的举办时间为\([s_i, f_i),0 \leqslant s_i < f_i\),而且假定集合\(S\)中的活动都已按结束时间递增的顺序排列好。因为某些缘由,这些活动在同一时刻只能有一个被举办,即对于任意两个活动\(a_i\)与\(a_j(i \neq j)\),区间\([s_i, f_i)\)与区间\([s_i, f_i)\)不能重叠,此时咱们称活动\(a_i\)与\(a_j\)兼容。在选择活动问题中,咱们但愿选择出一个最大兼容活动集。app
例如,给出以下活动集合\(S\):学习
\(i\) | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|
\(s_i\) | 1 | 3 | 0 | 5 | 3 | 5 | 6 | 8 | 8 | 2 | 12 |
\(f_i\) | 4 | 5 | 6 | 7 | 9 | 9 | 10 | 11 | 12 | 14 | 16 |
子集${a_3,a_9,a_{11} } $是相互兼容的,但它不是一个最大集,由于子集 \(a_i, a_4, a_8, a_{11}\)更大。实际上,\({a_1, a_4, a_8, a_{11}}\)是一个最大兼容活动集,另外一个最大集是:\(a_2, a_4, a_9, a_{11}\)。测试
咱们先尝试使用动态规划算法来解决该问题。首先验证该问题是否具备最优子结构性质。优化
令\(S_{ij}\)表示在\(a_i\)结束以后开始,且在\(a_j\)开始以前结束的活动集合。咱们的任务是求\(S_{ij}\)的一个最大兼容的活动子集。假设\(A_{ij}\)就是这样一个子集,其中包含\(a_k\),因而原问题就被分解为了两个子问题:求解 \(S_{ik}\)与\(S_{kj}\)中的兼容活动。显然,这两个子问题的兼容活动子集的并是原问题的一个兼容活动子集。spa
如今问题的关键是两个子问题的最优兼容活动子集的并是不是原问题的最优解。或者反过来,原问题的一个最优解\(A_{ij}\)是否包含两个子问题的最优解。答案是确定的,一样能够用剪切-粘贴证实,这里再也不赘述。设计
证实了该问题具备最优子结构性质,因而咱们能够用一个递归式来描述原问题的最优解。设\(c[i, j]\)表示集合\(S_{ij}\)的最优解的大小,则有:code
\[ c[i, j] = \begin {cases} 0 & \text{若$S_{ij} = \varnothing$ }\\ \max \limits_{a_k \in S_{ij}}\{c[i, k] + c[k, j] + 1\}&\text{若$S_{ij} \neq \varnothing$} \end{cases} \]递归
因而接下来就能够设计一个自顶向上的动态规划算法。
咱们看到,在上述的动态规划算法中,因为咱们不肯定到底选择哪个\(a_k\),会产生最优解,所以咱们必须考察每一种\(a_k\)的选取状况。天然地,咱们便会想,对于每一次\(a_k\)的选择,咱们可不能够直接找出“最优的\(a_k\)”的呢?若是能这样,那么算法的效率会大大地提高。
事实上,对于该问题,咱们在面临选择时,还真的能够只考虑一种选择(贪心选择)。直观上咱们能够想象,要想使活动更多的举办,咱们在每次选择活动时,应尽可能选择最先结束的活动,这样能够把更多的时间留给其余的活动。
更确切地说,对于活动集合\(S_{ij}\),因为活动都按照结束时间递增的顺序排列好,所以贪心选择是\(a_i\)。若是贪心选择是正确的,按照该选择方法,咱们便能将全部选择的结果组合成原问题的最优解。
如今问题的关键是,贪心选择选择出来的元素老是最优解的一部分吗?答案一样仍是确定的,下面的定理说明了这一点:
设\(S_k = \{a_i \in S:s_i \geq f_k\}\)表示在活动\(a_k\)结束后开始的活动集合。考虑任意非空子问题\(S_k\),令\(a_m\)是\(S_k\)中最先结束的活动,则\(a_m\)在\(S_k\)的某个最大兼容活动子集中。
咱们能够以下证实该定理:设\(A_i\)是\(S_i\)的一个最大兼容活动子集,且\(a_j\)是其中最先结束的活动。若\(a_m = a_j\),则天然知足上述结论;若\(a_m \neq a_j\),用\(a_m\)替代\(A_i\)中的\(a_j\),获得子集\(A'\),即 \(A' = (A - a_j) \cup a_m\),则\(A'\)也是\(S_i\)的一个最大兼容活动子集。由于\(a_m\)是\(S_k\)中最先结束的活动,因而有\(f_m \leq f_j\),所以\(A'\)仍然是兼容的。而且显然\(|A'| = |A|\),因此得出上面的结论,也就得出了定理中的结论。
有了上述分析的基础,咱们能够很容易设计出一个贪心算法来解决原问题。和动态规划算法相比,因为咱们每次都是一次性的找出了当时的最优解,而没必要像动态规划算法那样须要考虑每种可能的选择状况,所以贪心算法就没必要考虑子问题是否重叠,也就不须要解决重叠问题的“备忘录”了。所以,与动态规划算法相反的是,贪心算法一般都是自顶向下进行设计的。
下面给出一种Python的实现:
def recursive_activity_selector(s, f, k, n, ls): m = k + 1 while m <= n: if s[m] >= f[k]: ls.append(m) recursive_activity_selector(s, f, m, n, ls) return ls m += 1
对于文章开头给出的例子,作以下测试:
# 测试 if __name__ == '__main__': # 注意,这里添加了一个开始时间为0,结束时间也为0的活动。 s = [0, 1, 3, 0, 5, 3, 5, 6, 8, 8, 2, 12] f = [0, 4, 5, 6, 7, 9, 9, 10, 11, 12, 14, 16] k = 0 n = 11 ls = [] recursive_activity_selector(s, f, k, n, ls) print(ls)
打印结果为:
[1, 4, 8, 11]