Divide and Conquer
課堂補充 by TreapKing
Q&A Q&A
• 影片都看過了嗎?
• 對於影片的內容有沒有什麼問題?
• 雖然每次這樣問都沒人理我……
平面最近點對
Closest Pair (?
題目題目
• 在平面上給你 N 個點,要你找出歐式距離最短的兩個點。
• N <= 100,000
naïve 作法 naïve 作法
• 開心 CN 取 2 = O(N2)
• 顯然會壞掉 QQ
• 如果在平面上想要做 divide and conquer ,該怎麼分割問題?
平面 D&C 平面 D&C
• 我們可以先把所有輸入的點照 x 座標排序後,在中間畫一條分 隔線。
• 這樣可能的答案就分成 ( 兩個點都在左邊 ) 、 ( 兩個點都在右 邊 ) 、 ( 橫跨分隔線 ) 三種情況
• 一些平面上的 D&C 題都會做類似的事。
• 顯然只有點對橫跨分隔線的情況需要討論,如果這個情況可以解 決的話,其他兩個情況只要遞迴下去解就好了。
思考一下思考一下
• 如果只是要算分隔線兩邊的最近點對,有什麼好方法嗎?
• 因為 x 座標的大小關係已經確立了,所以可以把兩邊直接照 y 座標排序
• 然後就發現還是好困難喔 .___.
剪枝!?剪枝!?
• 定神一想,會發現如果遞迴下去後找到的最近點對的距離是 d
,那麼我們根本就不需要考慮那些距離超過 d 的點對
• 所以離分隔線超過 d 的點都不需要去考慮。
• 對於每個點,也只有 y 座標差距不超過 d 的點可能可以讓你 找到更近的點對
• 聽起來只是個壓常數的剪枝,但在什麼情況下,這個做法一樣會 退化成 N2 呢?
• 因為已經知道各自的最近點對的距離是 d ,所以每個點附近的 點不會太多 !
• 附近的點只會有常數個,所以直接跑下去複雜度就是好的!
多項式乘法
Polynomial Multiplication
多項式乘法 多項式乘法
• 給你兩個 n 次多項式,請你印出兩個多項式的乘積。
• Ex:
不就只是乘法 不就只是乘法
• Naïve 作法
• 用個雙重迴圈跑一跑乘一乘加一加
• 時間複雜度是 O(n2)
• 據說在古早的時代,從來沒有人質疑過乘法可以做得更快
• 可能會有人想到 FFT ,但我現在沒有要講這個 XD
一些小觀察 一些小觀察
• 把多項式表達為
• 同理:
• 順便不失一般性的假設 n+1 是 2 的次方 ( 如果不是的話,就加入 一些係數為 0 的高次項 )
一些小觀察 一些小觀察
• 既然是要教分而治之 (fun and j_zz)法,那就先把多項式切兩半看看
• 令
• 同理
• 所以
一些小觀察 一些小觀察
• A, B, C, D 都是 (n/2) 次的多項式
• 所以只要做四次比較小的乘法就可以解決原問題了!
• 成功切成子問題 => Divide & Conquer!
• 可是,這樣真的有比較快嗎?
• 用個很不嚴謹的感覺會發現,我們好像沒有真的減少了什麼運算
• 事實上這樣做的複雜度還是 O(n2) 的
更多觀察更多觀察
• 我們想要算的東西是
• 如果我們計算
• 就可以把原本的式子化為
• 因為加 / 減法只需要花 O(n) ,這樣子我們只用了 3 個乘法
D&C on 多項式乘法 D&C on 多項式乘法
• 看起來我們得到了一個比較快的做法
• 那麼,這樣的時間複雜度是多少呢?
• 設這個做法解 n 次多項式花的時間是 T(n) ,那麼我們會得到這 個遞迴式:
• 如果你已經猜到某個複雜度的話,可以用歸納法去證明它
• 這裡介紹一個更強的工具
主定理 (Master Theorem) 主定理 (Master Theorem)
reference: 隨機客的演算法投影片
帶個數字帶個數字
• 對於
• 可以發現顯然是對應到主定理的 Case 1
• 因此我們可以得知這個做法的複雜度為
樹分治
Divide and Conquer on Tree
樹分治樹分治
• 顧名思義是在樹上做分治
• 一如往常先給一個例題再說
例題例題
• 給你一顆 N 個點的樹,每條邊的邊權都是 1 ,問你這棵樹上 有幾條路的路徑不大於 k
• N, k <= 100,000
來點 naïve 作法 來點 naïve 作法
• 面對這題可以怎麼做呢?
• 枚舉 N2 種路徑, O(N) 算一下他們的長度,總複雜度 O(N3)
• 枚舉起點, DFS or BFS 遍歷整棵樹數有幾條路徑長 <=k , O(N2)
• 不過我們顯然要找到一個不用枚舉路徑的方法
• 這週的主題是分治,所以一定是要找一個分治的解法!
試著亂分治一通 試著亂分治一通
• 如果把原本的樹分成兩棵樹看看。
• 假設原本的樹被一條 a-b 邊切開,那麼答案就等於 (a 那邊的路徑數 )+(b 那邊 的路徑數 )+( 經過 a-b 邊的路徑數 )
• 前兩項可以分治下去,第三項可以怎麼算呢?
• 花 O( 子樹大小 ) 可以找到所有以 a 或 b 為起點的所有路徑
• 設 da[k] 為以 a 為起點,長度為 k 的路徑的數量, db 同理
• 跨 a-b 邊且長度不大於 k 的路徑數 = da[0]*db[0:k-2] + da[1]*db[0:k-3]
+ ……
• 所有操作都是 O( 子樹大小 )
• 如果每次都可以找到好的 a-b 邊把整顆樹切一半的話,複雜度就會是 O(NlgN)
所以要怎麼砍樹?
所以要怎麼砍樹?
• 要怎麼找 a-b 邊讓每次的樹都盡量變兩半呢?
• 定神想一分鐘,就會發現星狀圖直接沒救
• 如果不是找一條邊,而是找出一個點呢?
• 以防又白想了一陣,先確保我們可以找到「好點」
• 所以一定存在一個好點,拔掉這個點後,剩下的子樹都不大於原 本的樹的大小的一半
• 總之好點就是存在啦,如果你是沒來上課事後才載投影片下來看 的話,那就請你自己想辦法證一證
step by step step by step
• 套回分治的框架
• 1. 找到一個好點 g
• 2. Divide ,把這個點的所有兒子子樹們遞迴下去求解
• 3. Merge ,計算所有通過 g 且長度不大於 k 的路徑數
• 真正的問題在 1. & 3.
good point good point
• 用一個 dfs 找出對於每個點 u ,以 u 為根的子樹的大小,設為 sz [u]
• 對於每個點 v ,設 v 的兒子們為 t1, t2, …
• 拔掉 v 後,剩下的子樹 size 為 N-sz[v], sz[t1], sz[t2], …
• 先前證明過一定有個點可以讓剩下的子樹大小都不超過原本的一 半
merge merge
• 剛剛我們已經介紹過兩個子樹要怎麼合併,那 t 個子樹呢?
• 直接 Ct 取 2 的話複雜度會慘掉
• 先做 1 - 2 ,再做 1&2 – 3 ,再做 1&2&3 – 4……
• 這樣複雜度就會是好的!
實作上的細節 實作上的細節
• 算子樹的時候,可以把拔掉的點設成不能走,各種 dfs 的時候不 要走到被拔掉的點上面
• 注意一下遞迴的時候做事情的順序,有些全域陣列可能遞迴下去 之後會被改掉。