or
or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up
Syntax | Example | Reference | |
---|---|---|---|
# Header | Header | 基本排版 | |
- Unordered List |
|
||
1. Ordered List |
|
||
- [ ] Todo List |
|
||
> Blockquote | Blockquote |
||
**Bold font** | Bold font | ||
*Italics font* | Italics font | ||
~~Strikethrough~~ | |||
19^th^ | 19th | ||
H~2~O | H2O | ||
++Inserted text++ | Inserted text | ||
==Marked text== | Marked text | ||
[link text](https:// "title") | Link | ||
 | Image | ||
`Code` | Code |
在筆記中貼入程式碼 | |
```javascript var i = 0; ``` |
|
||
:smile: | ![]() |
Emoji list | |
{%youtube youtube_id %} | Externals | ||
$L^aT_eX$ | LaTeX | ||
:::info This is a alert area. ::: |
This is a alert area. |
On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?
Please give us some advice and help us improve HackMD.
Do you want to remove this version name and description?
Syncing
xxxxxxxxxx
分治法內容
分治法 (Divide and Conquer)
分治是一種非常重要的演算法思維模式與策略,有很多重要的演算法都是根據分治的思維模式,例如快速排序法、合併排序法、快速傅立葉轉換(FFT)、矩陣乘法、整數乘法以及在一些在計算幾何的知名演算法都是分治的策略。
當碰到一個不曾見過的問題,分治往往是第一個思考的重點,因為分治的架構讓我們很容易找到比暴力要好的方法。
核心想法
分治法主要有三個步驟,切割,解決,合併。
代碼
簡單例子1 (找序列最大/小值)
題目 : 給定長度為 N 的序列,找到最大值/最小值
想法
按照分治法三個步驟,切割,解決,合併,把問題大化小。
代碼 (計算最大值演示)
簡單例子2 (合併排序法)
在排序那篇中,有提到合併排序法,合併排序法的根本原理就是分治法
想法
按照分治法三個步驟,切割,解決,合併,把問題大化小。
合併:
合併的時候,會拿到左右兩區間 "排序好的序列" ,這邊可以利用排序好的特性來進行合併。
假設目前左區間回傳 [ 1, 3, 8, 9]
右區間回傳 [2, 4, 6,]
合併:
[ 1, 3, 8, 9]
[2, 4, 6]
利用已經排序的特性,左邊一定比右邊小,所以可以把兩個序列從左邊比到右邊,放進新的序列中。
第一輪
[ 1 , 3, 8, 9]
[ 2 , 4, 6]
新序列
[ 1 ]
第二輪
[1, 3 , 8, 9]
[ 2 , 4, 6]
新序列
[1, 2 ]
第三輪
[1, 3 , 8, 9]
[2, 4 , 6]
新序列
[1, 2, 3 ]
第四輪
[1, 3, 8 , 9]
[2, 4 , 6]
新序列
[1, 2, 3, 4 ]
第五輪
[1, 3, 8 , 9]
[2, 4, 6 ]
新序列
[1, 2, 3, 4, 6 ]
第六輪
[1, 3, 8 , 9]
[2, 4, 6]
新序列
[1, 2, 3, 4, 6, 8 ]
第七輪
[1, 3, 8, 9 ]
[2, 4, 6]
新序列
[1, 2, 3, 4, 6, 8, 9 ]
複雜度
這兩個簡單的例子可以推到
複雜度
如果分割的很少,那每個問題的"量"就會很多,
如果分割的很多,那每個問題的"量"雖然少,但合併次數也會很多,
到底要怎麼來計算複雜度呢?
複雜度
分治是一個遞迴的演算法,不像迴圈的複雜度可以用加總的方法或是乘法計算,遞迴的複雜度是由遞迴關係式(recurrence relation)所表達。計算複雜度必須解遞迴關係式。遞迴關係又稱為差分方程式(difference equation),解遞迴關係是個複雜的事情。
分治演算法的常見形式是將一個大小為 n 的問題切成 a 個大小為 b 的子問題此外必須做一些分割與合併的工作。假設大小為 n 的問題的複雜度是 T(n),而分割合併需要的時間是 f(n),可以得到以下遞迴關係:
\(T(n) = aT(\frac{n}{b}) + f(n)\)
主定理 (Master Method)
原理就是根據剛剛說到的,要來評估分割,合併的關係。
一個分治的問題可以寫成
\(T(n) = aT(\frac{n}{b}) + f(n)\)
主定理分成三種情況
情況一
(簡單說,合併解決問題 < 分割)
如果存在 ε > 0 使得
\(f(n) = O(n^{log_b (a) - ε})\)
則
\(T(n) = Θ(n^{log_b a})\)
情況二
(合併解決問題 = 分割)
如果存在 ε \(\geq\) 0 使得
\(f(n) = Θ(n^{log_b a} log^εn)\)
則
\(T(n) = Θ(n^{log_b a} log^{ε+1}n)\)
情況三
(合併解決問題 > 分割)
如果存在 ε > 0 使得
\(f(n) = Ω(n^{log_b (a) + ε})\)
且存在 c < 1 滿足
\(af(\frac{n}{b}) \leq cf(n)\)
\(T(n) = Θ(f(n))\)
複雜度分析-找最大值/最小值
\(T(n) = aT(\frac{n}{b}) + f(n)\)
根據切割和合併寫下
\(T(n) = 2T(\frac{n}{2}) + Θ(1)\)
根據情況一
存在 ε > 0 使得 \(f(n) = O(n^{log_2 (2) - ε})\)
所以 \(T(n) = Θ(n^{log_2 2}) = Θ(n)\)
複雜度分析-合併排序法
\(T(n) = aT(\frac{n}{b}) + f(n)\)
根據切割和合併寫下
\(T(n) = 2T(\frac{n}{2}) + Θ(n)\)
根據情況二
存在 ε = 0 使得
\(f(n) = Θ(n^{log_2 2} log^εn)\)
則
\(T(n) = Θ(n^{log_2 2} log^{ε+1}n) = Θ(nlogn)\)
題外話
主定理並不是萬能的,還是會有很多例外,複雜一點就變得很難評估
也有另外一種算法 Recursion-Tree Method,如果有興趣的話可以自行查詢。
費式數列
前面提過很多很多次,已經學會了最原始遞迴,後來的動態規劃,再到用線性代數的方式和矩陣快速冪求解。
費式數列是一個很酷的數列,無處不在,甚至和黃金比例也產生了關係,接下來就來利用費式數列的一些性質,導出一些式子,就可以利用分治法求解。
為了方便,定義費式數列從第一項開始,也就是
\(F_1 = F_2 = 1\)
性質一
(不會用到,但有趣提一下)
\(F_{n+2} = \sum_{k=1}^n F_k + 1\)
證明
首先有
\(F_{n+2} = F_{n+1} + F_{n}\)
如果展開 n + 1 項
\(F_{n+2} = F_{n} + F_{n-1} + F_{n}\)
繼續展開直到 n = 2
\(F_{n+2} = F_{2} + F_{1} + ... + F_{n-1} + F_{n}\)
定義得 \(F_{2} = 1\) 得到
\(F_{n+2} = 1 + F_{1} + ... + F_{n-1} + F_{n}\)
性質二
第(m+n)項等於第 m 項乘上第(n-1)項後再加上第(m+1)
項乘上第 n 項之和
\(F_{m+n} = F_m F_{n-1} + F_{m+1}F_{n}\)
證明
利用 \(F_1 = F_2 = 1\),以及費式數列定義得
\(F_{m+n} = F_{1}F_{m+n-2}+F_{2}F_{m+n-1}\)
= \(F_{1}F_{m+n-2} + F_{2}(F_{m+n-2} + F_{m+n-3})\)
= \(F_{m+n-2}(F_1 + F_2) + F_{2}F_{m+n-3}\)
= \(F_{2}F_{m+n-3} + F_3F_{m+n-2}\)
會發現是不是又跟一開始長的形式一樣,所以繼續推下去可以得到
= \(F_{m-1}F_{n} + F_mF_{n-1}\)
奇偶項恆等式
\(F_{2n-1} = F_n^2 + F_{n-1}^2\)
\(F_{2n} = (F_{n-1} + F_{n+1})F_n = (2F_{n-1} + F_{n})F_n\)
證明
由性質二 \(F_{m+n} = F_m F_{n-1} + F_{m+1}F_{n}\)
奇數項令 m = n-1 直接得證
偶數項令 m = n ,然後展開 n + 1 項得證
分治法求解
剛剛得到
\(F_{2n-1} = F_n^2 + F_{n-1}^2\)
\(F_{2n} = (F_{n-1} + F_{n+1})F_n = (2F_{n-1} + F_{n})F_n\)
所以發現可以直接根據奇數偶數分治下去
實作
複雜度分析
很不幸的,目前複雜度還是 \(O(N)\),雖然分成了一半一半,但並沒有做到任何可以加快的部分。
主定理
\(T(n) = 2T(\frac{n}{2}) + O(1)\) 這個跟找最小/大值一樣。
所以時間複雜度真的為 \(O(N)\)
加快
會發現雖然分成了一半,可以讓 n 快速下降,但是總數量卻是不變的,不過,雖然總數量一樣,但是卻是會重複非常多次,所以如果能夠紀錄走過的路,那就可以優化了!
這邊使用 map 來儲存,因為除二的結果不重複的數量大概為 \(logn\)
但是如果想用陣列儲存會因為 n 太大沒辦法存,所以只能使用速度較慢的 map 來存,但好在數量小,所以不太影響。
實作
時間複雜度
\(O(logn)\)
因為每次都除二,所以總數量大概為 logn
雖然 map 的查詢,儲存時間為 \(O(NlogN)\) 但是因為 \(N = logn\),和題目總數 n ~ 1E12,實在是小到可以忽略,可以當成 O(1) (嚴謹一點當然可以寫成 \(O(logn \times loglogn)\),放去主定理推也是可以得到一樣的結果)
主定理
分成了左右兩半,但另外一邊反而會因為記錄過所以不會重複算到
\(T(n) = T(n/2) + O(1)\)
根據情況二, ε = 0,可得時間複雜度為 \(O(logn)\)
實測
當然也可以用程式碼來粗估複雜度
優缺點
優點
較為直觀,就是核心三步驟(分割,求解,合併),通用問題全部都可以用。
缺點
分治法的題目較為難寫,同時並不是說分治下去的時間複雜度就會好,還是需要去評估的。
另外,雖然複雜度一樣,但只要有分出去的動作,就是一種 cost ,所以正常情況下,如果有不需要分治的狀況,分治的常數往往都是較差的一方。
常見優化方式
比較 (貪心,DP,分治)
總結
分治對於初學者來說較有難度,但是對於基本功,看待遞迴複雜度很有幫助。
分治法有很多延伸,像是線段樹、CDQ分治法、DP優化,FFT,NTT,Karatsuba,甚至數值方法中也很常用像是勘根法,也都是建立在分治上,可以說是非常實用(且困難)的算法。
練習
挑戰題 (經典題)