貪心算法(1)——算法導論(21)

1. 寫在前面

在以前的5篇博客中,咱們學習了動態規劃算法。咱們能夠看到,在求解最優化問題的算法中,一般須要通過一系列的步驟,在每一個步驟中都面臨多種選擇。對於許多最優化問題,使用動態規劃算法來求解最優解有些殺雞用牛了,可使用更加簡單的算法。貪心算法(greedy algorithm)就是其中之一:它在每一步作出的選擇都是當時看起來的最優選擇。也便是說,它老是作出局部最優選擇,以此但願這樣的選擇可以產生全局的最優解。python

2. 選擇活動問題

2.1. 提出問題

咱們先來看看一個適應貪心算法求解的問題:選擇活動問題算法

假定有一個\(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}\)測試

2.2. 分析問題

2.2.1. 動態規劃算法

咱們先嚐試使用動態規劃算法來解決該問題。首先驗證該問題是否具備最優子結構性質優化

\(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} \]遞歸

因而接下來就能夠設計一個自頂向上的動態規劃算法。

2.2.2. 貪心選擇

咱們看到,在上述的動態規劃算法中,因爲咱們不肯定到底選擇哪個\(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|\),因此得出上面的結論,也就得出了定理中的結論。

2.3 解決問題

有了上述分析的基礎,咱們能夠很容易設計出一個貪心算法來解決原問題。和動態規劃算法相比,因爲咱們每次都是一次性的找出了當時的最優解,而沒必要像動態規劃算法那樣須要考慮每種可能的選擇狀況,所以貪心算法就沒必要考慮子問題是否重疊,也就不須要解決重疊問題的「備忘錄」了。所以,與動態規劃算法相反的是,貪心算法一般都是自頂向下進行設計的。

下面給出一種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]
相關文章
相關標籤/搜索