分治法

Hang3發表於2024-10-09

演算法導論

這個文件是學習“演算法設計與分析”課程時做的筆記,文件中包含的內容包括課堂上的一些比較重要的知識、例題以及課後作業的題解。主要的參考資料是 Introduction to algorithms-3rd(Thomas H.)(對應的中文版《演算法導論第三版》),除了這本書,還有的參考資料就是 Algorithms design techniques and analysis (M.H. Alsuwaiyel)。

分治法

分治法(Divide-and-Conquer)是一種十分有用的演算法思想,比如歸併排序所體現出來的演算法思想就是分治。

在分治的演算法思想下,遞迴性地解決問題,在每一次遞迴都執行下面的三個步驟:

  1. Divide: 將當前的問題劃分為若干的子問題,這些子問題都是同一個問題的更小的例項,或者說是更小規模的相同問題
  2. Conquer: 遞迴性地解決子問題,也就是將子問題傳到下一層遞迴中。如果子問題的規模已經足夠小了,那麼就直接解決這個子問題
  3. Combine: 將子問題的解決結果合併為當前問題的解決結果

這就是分治演算法的基本思想,另外,在分治思想中還涉及到兩個概念:

  • recursive case: 就是當問題規模太大而無法直接解決,只能用遞迴解決的情況
  • base case: 就是當問題規模足夠小,能夠直接解決的情況

比如歸併排序就是一種比較典型的分治思想的演算法,把對一個序列的排序工作劃分成對兩個子序列的排序工作:

\[T(n) = \begin{cases} \Theta(1) & if\space n=1\\ 2T(n/2) + \Theta(n) & if\space n>1 \end{cases} \]

即,如果序列的長度為1,那麼就無需排序,需要的時間複雜度為\(\Theta(1)\),而如果序列的長度大於1,那麼就這個序列劃分為兩個子序列,並且對子序列進行排序,然後將排好序的子序列進行合併,合併兩個有序的子序列需要的時間複雜度為\(\Theta(n)\)

而上面的式子也被稱為遞迴式

遞迴式求解

下面介紹如何求解遞迴式,也就是透過遞迴式計算演算法整體的時間複雜度的方法:

比如說有下面這樣的一個遞迴式:

\[T(n) = \begin{cases}d & if\space n=1\\ aT(n/b) + f(n) & if\space n>1\end{cases} \]

在這個式子中,原問題被分解為a個子問題,每個子問題的規模為原來的 1/b 倍,而將這個a個子問題的解合併成原問題的解需要的時間為f(n),而解決一個base case所需的時間為常數d。

最容易想到的方法就是對遞迴式進行展開:

\[\begin{align} T(n) &=aT(\frac{n}{b}) + f(n)\\ T(\frac{n}{b})&=a(\frac{n}{b^2}) + f(\frac{n}{b})\\ \cdots\\ T(n)&=a^kT(\frac{n}{b^k})+\sum_{i=0}^{k}a^if(\frac{n}{b^i}) \end{align} \]

不過這樣的方法比較抽象,可能需要分很多種情況進行討論。

Introduction to algorithm 一書中,介紹了三種求解遞迴的方法,

  • substitution method:猜測一個界限,然後使用數學歸納法來證明這個猜測是正確的
  • recursive-tree method:使用遞迴樹來,遞迴樹中的節點表示在遞迴的各個級別上產生的成本,然後使用邊界求和的方法來求解
  • master method:可以用於計算形如\(T(n) = aT(n/b) + f(n)\)形式的遞迴式的邊界,在這個式子中,原問題被分解為a個子問題,每個子問題的規模為原來的 1/b 倍,而將這個a個子問題的解合併成原問題的解需要的時間為f(n)

substitution method通常用於證明漸進上界和漸進下界,當然也可以用於證明緊漸進界。

這裡介紹一個使用substitution method方法求解遞迴式的例子,遞迴式如下:

\[f(n) = \begin{cases} d & if\space n = 1\\ f(\lfloor n/2\rfloor)+f(\lceil n/2 \rceil) + bn & if\space n\ge 2 \end{cases} \]

對於非負常數 bd,如果 n 為2的整數冪,那麼這個遞迴式可以簡化為:

\(f(n) = 2f(n/2) + bn\)

那麼我們可以假設:

\(f(n) \le cbn\log n + dn\),其中 c 為大於0的常數,並且接下來將會確定 c 的值。

這部分的內容主要摘自 Algorithms design techniques and analysis,這裡介紹了一些推論,用於假設遞迴式的邊界,但是這裡由於篇幅限制,所以沒進行介紹。

假設上面猜測的結果在\(n\ge 2\)時,對於\(\lfloor n/2 \rfloor\)\(\lceil n/2 \rceil\)是成立的,那麼我們將其帶入原式子右邊的子式子可得:

\[\begin{align} f(n) = & f(\lfloor n/2 \rfloor) + f(\lceil n/2 \rceil) + bn\\ \le & cb\lfloor n/2 \rfloor\log (\lfloor n/2 \rfloor) + d\lfloor n/2\rfloor + cb\lceil n/2 \rceil\log (\lceil n/2 \rceil) +d\lceil n/2 \rceil + bn\\ \le&cb\lfloor n/2 \rfloor\log (\lceil n/2 \rceil) + cb\lceil n/2 \rceil\log (\lceil n/2 \rceil) + dn + bn\\ =&cbn\log(\lceil n/2 \rceil) + dn + bn\\ \le&cbn\log ((n+1)/2) + dn + bn\\ =&cbn\log (n+1) - cbn + dn + bn \end{align} \]

欲使前面的假設成立,則有:

\[\begin{align} cbn\log (n+1) - cbn + dn + bn \le& cbn\log n + dn\\ cbn\log(n+1) - cbn + bn \le& cbn\log n\\ c\log (n+1) -c + 1\le& c\log n\\ c\ge& \frac{1}{1 + \log n - \log(n+1)} = \frac{1}{1 + \log\frac{n}{n+1}} \end{align} \]

\(n\ge 2\)時,

\(\frac{1}{1+\log\frac{n}{n+1}} \le \frac{1}{1+\log\frac{2}{3}} \lt 2.41\)

於是取c = 2.41。並且當n = 1時,\(f(n) = d\le cb\log 1 + d = d\),假設依然成立。

所以能夠求解出上面遞迴式的上界為\(f(n)\le 2.41 bn\log n + dn\)

現在計算遞迴式的漸進下界,同樣,假設遞迴式\(f(n)\)的下界為\(cbn\log n + dn\),其中 c 為大於0的常數,後面會確定其取值。

假設上面的猜測在\(n\ge 2\)時,對於\(\lfloor n/2 \rfloor\)\(\lceil n/2 \rceil\) 是成立的,那麼帶入遞迴式右邊可得:

\[\begin{align} f(n)=&f(\lfloor n/2 \rfloor) + f(\lceil n/2 \rceil) + bn\\ \ge&cb\lfloor n/2 \rfloor\log(\lfloor n/2 \rfloor) + d\lfloor n/2 \rfloor + cb\lceil n/2 \rceil\log(\lceil n/2 \rceil) + d\lceil n/2 \rceil + bn\\ \ge&cb\lfloor n/2 \rfloor\log(\lfloor n/2 \rfloor) + d\lfloor n/2 \rfloor + cb\lceil n/2 \rceil\log(\lfloor n/2 \rfloor) + d\lceil n/2 \rceil + bn\\ =&cbn\log (\lfloor n/2 \rfloor) + dn + bn\\ \ge&cbn\log(n/4) + dn +bn\\ =&cbn\log n - 2cbn + dn + bn \end{align} \]

欲使上面的假設成立,則有:

\[\begin{align} bn - 2cbn &\ge 0\\ c&\le \frac{1}{2} \end{align} \]

於是取 \(c =\frac{1}{2}\),則當\(n\ge 2\)時,\(f(n)\ge \frac{1}{2} bn\log n + dn\) 成立,並且當\(n= 1\)時,\(f(n) = d \ge \frac{1}{2}b\log 1 + d =d\),假設依然成立。

所以能夠求出上面的遞迴式的下界為\(f(n)\ge \frac{1}{2}bn\log n + dn\)

綜上,上述遞迴式的緊漸進邊界為\(f(n) =\Theta(n\log n)\)

這種方法的困難在於提出一個巧妙的猜測,將這個猜測作為遞迴式的嚴格邊界。然而,在多數情況下,給定遞迴式類似於另一個遞迴式,而這個遞迴式的解是已經提前知道了的,所以可以藉助這個遞迴式的解,來假設當前遞迴式的解,上面介紹的這個例子就是這樣的。

在上面的這個三個方法中,最常用並且也是最有用的方法就是主方法(master method),這個方法通常用於計算遞迴式的漸進邊界。

主定理

主定理主要用於求解下面這種形式的遞迴式:

\(T(n)=aT(n/b) + f(n)\)

其中a, b是大於等於1的常數,而 f(n) 是一個漸進的正函式。

將T(n)分下面三種情況進行討論:

  • \(f(n) = O(n^{\log_b a-\epsilon})\),其中 \(\epsilon \gt 0\),則 \(T(n)=\Theta(n^{\log_b a})\)
  • \(f(n)=\Theta(n^{\log_b a})\),則 \(T(n)=\Theta(n^{\log_b a}\lg n)\)
  • \(f(n)=\Omega(n^{\log_b a +\epsilon})\),其中 \(\epsilon \gt 0\),並且如果 \(af(n/b) \le cf(n)\),對於大於1的常數 c 以及足夠大的 n 滿足,則 \(T(n)=\Theta(f(n))\)

下面介紹一些使用主定理求解遞迴式的例子:

# 1. \(T(n) = 9T(n/3)+n\)

則: \(n^{\log_ba} = n^{\log_3 9}=n^2\)

\(f(n)=n=O(n^{\log_39 -\epsilon})\),其中\(\epsilon =1\)

根據主定理的第一個情況,\(T(n)=\Theta(n^2)\)

# 2. \(T(n)=T(2n/3)+1\)

則: \(n^{\log_ba}=n^{\log_{3/2}1} = 1\)

\(f(n) = 1 = \Theta(1)\)

根據主定理的第二個情況,\(T(n)=\Theta(n^{\log_{3/2}1}\lg n) = \Theta(\lg n)\)

# 3. \(T(n)=3T(n/4)+n\lg n\)

則: \(n^{\log_b a} = n^{\log_43} = O(n)\)

\(f(n) = n\lg n = \Omega(n^{\log_43+\epsilon})\),其中\(\epsilon = 1 - \log_43\approx 0.2\)

所以可以應用主定理的第三種情況。

\(af(n/b) = 3(n/4)\lg(n/4) = (3/4)n\lg(n/4) \le (3/4)n\lg n = cf(n)\),其中 \(c = 3/4\)

根據主定理的第三種情況,\(T(n) = \Theta(n\lg n)\)

Selection Problem

選擇問題(Selection Problem):找到一個大小為 n 陣列中的第 k 小的元素。

那麼最直接的方法是,先將這個陣列進行排序,然後取第 k 個元素,那麼這樣的方法的時間複雜度為\(\Omega(n\log n)\),這也是所有基於比較的排序演算法在最壞情況下的時間複雜度。

然而事實上,找到第 k 小的元素可以有線性時間複雜度的最優解。因為一個最簡單的道理是,如果對原陣列進行了排序,那麼我們不僅能夠找到第 k 小的元素,我們也能找到第 k+1 小的元素等,所以對陣列進行排序的操作存在著一些冗餘的工作量。

下面介紹這個問題最優解法的思路:

- 如果陣列中的元素數量小於44個(44是一個預定義的閾值,後面會解釋為什麼會將這個閾值),那麼就直接對陣列進行排序,然後找第 k 個元素

- 否則,找到這個陣列比較靠近"中間"的元素記為 mm,然後透過下面的方法將陣列劃分為三組:

- $A_1=\{a|a \lt mm\}$
- $A_2=\{a|a=mm\}$
- $A_3=\{a|a\gt mm\}$

那麼,可以分下面幾種情況討論:

\#1. 若$|A_1| \ge k$,那麼陣列中第 *k* 小的元素必然在集合 $A_1$中,繼續在 $A_1$ 中找第 *k* 小的元素

\#2. 若$|A_1| \lt k, |A_1| + |A_2|\ge k$,那麼陣列中第 *k* 小的元素就在 $A_2$ 中,所以第 *k* 小的元素就是 *mm*

\#3. 若$|A_1 + A_2| \gt k$,那麼陣列中第 *k* 小的元素必然在 $A_2$ 中,繼續在$A_2$ 中找第 $k - |A_1| - |A_2|$ 小的元素

下面將介紹如何找到陣列中靠近"中間"的元素:

將這個陣列中每5個元素分為一組,如果陣列長度不能被5整除,那麼就丟棄剩下的元素。

找到每個小組的中位數,將這些中位數放在一個集合中,這個集合記為m,那麼我們要找的元素 mm 就是集合 m 的中位數。

演算法虛擬碼如下:

selection_problem

下面透過一個例子來演示這個演算法的過程,並且為了方便演示而取消了閾值判斷過程:

假設陣列 A = { 8, 33, 17, 51, 57, 49, 35, 11, 25, 37, 14, 3, 2, 13, 52, 12, 6, 29, 32, 54, 5, 16, 22, 23, 7 }

要找到陣列A中的第13小的元素,也就是陣列A的中位數。

先將A五五分組:

(8, 33, 17, 51, 57), (49, 35, 11, 25, 37), (14, 3, 2, 13, 52), (12, 6, 29, 32, 54), (5, 16, 22, 23, 7)

然後找到每個小組的中位數(注意到這裡每個小組只有5個元素,所以找中位數的時間開銷可以視為一個固定常數),並且放入集合中:M={ 33, 35, 13, 29, 16 }

M 排序,M={ 13, 16, 29, 33, 35 },則 mm = 29

現在對原陣列 A 進行劃分:

\(A_1=\{ 8, 17, 11, 25, 14, 3, 2, 13, 12, 6, 5, 16, 22, 23, 7 \}\)

\(A_2=\{ 29 \}\)

\(A_3=\{ 33, 51, 57, 49, 35, 37, 52, 32, 54 \}\)

因為\(|A_1| = 15 > 13\),所以丟棄\(A_2,A_3\),在\(A_1\) 中找第13小的元素。

\(A = A_1\) 並五五分組:

(8, 17, 11, 25, 14), (3, 2, 13, 12, 6), (5, 16, 22, 23, 7)

找到每個小組的中位數,並且放入集合中:M = { 14, 6, 16 }

mm =14

現在對陣列 A 進行劃分:

\(A_1=\{ 8, 11, 3, 2, 13, 12, 6, 5, 7 \}\)

\(A_2=\{14\}\)

\(A_3=\{ 17, 25, 16, 22, 23 \}\)

因為\(|A_1| + |A_2| = 10 < 13\),所以丟棄\(A_1, A_2\),在\(A_3\) 中找第13-10=3小的元素

顯然\(A_3[3]=22\)

所以原陣列第13小的元素為22。

Algorithm analysis

顯然,不難驗證上面演算法的正確性。

現在來分析上面這個演算法的時間複雜度。

想要分析出分治演算法的時間複雜度,那麼就需要先確定其子問題的大小,如下圖:

selection_problem_illustration

上圖中,所有元素每列是按照從下往上遞增排序的,然後將每列按照該列中位數的大小從左到右遞增排序,那麼對於整個集合的中間數 mm,圖中左下角(W)的元素都是小於或等於 mm 的元素,圖中右上角(X)的元素都是大於或等於 mm 的元素。

而這個演算法中的子問題規模是所有嚴格小於中間數 mm 的元素集合( A1 )大小,或者嚴格大於中間數 mm 的元素集合( A3 )大小。現在考慮所有小於或等於中間數 mm 的集合( A1' ),顯然,這個集合至少是和集合 W 一樣大的。即:

\(|A1'| \ge 3\lceil \lfloor n/5 \rfloor / 2 \rceil \ge \frac{3}{2}\lfloor n/5 \rfloor\)

那麼所有嚴格大於中間數 mm 的元素集合就是所有小於或等於中間數 mm 的元素集合的補集,即:

\(|A3| \le n - \frac{3}{2}\lfloor n/5 \rfloor \le n - \frac{3}{2}(\frac{n-4}{5}) = n - 0.3n + 1.2 = 0.7 n + 1.2\)

同理可得:

\(|A1| \le n - \frac{3}{2}\lfloor n/5 \rfloor \le n - \frac{3}{2}(\frac{n-4}{5}) = n - 0.3n + 1.2 = 0.7 n + 1.2\)

現在可以開始計算整個演算法的時間複雜度了:

首先整個集合的規模( n )是知道的;將整個集合五五分組的時間複雜度是\(\Theta(n)\);將每組的 5 個元素進行排序的時間複雜度是 \(\Theta(n)\),因為對 5 個元素排序最多隻需要 7 步,可以視為一個常數開銷;找到所有中位數的中位數的開銷是 \(T(\lfloor n/5 \rfloor)\),使用這裡的選擇演算法找中位數;在確定了中間數 mm 後,就可以進入子問題了,根據前面的分析,子問題的時間複雜度應該是 \(T(0.7n + 1.2)\)。即:

\(T(n) = T(\lfloor n/5 \rfloor) + T(0.7 n + 1.2) + cn\)

其中 c 為足夠大的常數。

為了將 0.7n+1.2 轉換成n的常數倍形式,這裡假設:

\(0.7n + 1.2 \le \lfloor 0.75n \rfloor\)

\(n = 44\)時,\(0.7n + 1.2 \le 0.75n - 1\) 成立。

因此,演算法的時間複雜度為:

\[T(n) = \begin{cases} c& n \lt 44\\ T(\lfloor n/5 \rfloor) + T(\lfloor 3n/4 \rfloor) + cn &n\ge 44 \end{cases} \]

因為 \(\frac{1}{5} + \frac{3}{4} < 1\),所以演算法的時間複雜度為 \(T(n) = \Theta(n)\)

相關文章