<style> .reveal .slides { text-align: left; font-size:28px; } </style> # 分治法(Divide&Conquer) Introduction to Competitive Programming --- ## 核心概念 - 分(divide):將大問題分成小問題 - 治(conquer):遞迴計算出小問題的答案 - 合(merge):將小問題的答案合併成大問題的答案 - 通常子問題的大小是大問題的分數倍 ($\frac{n}{b}$) ---- 將分治套用在序列上 - 分(divide):將大問題分成小問題 通常序列上的問題都會把序列分成兩半,左半&右半 - 治(conquer):遞迴計算出小問題的答案 分成左半序列&右半序列後,分別遞迴求出各自的答案 - 合(merge):將小問題的答案合併成大問題的答案 求出左右兩半各自的答案後,合併跨越兩半的答案 ---- ## 套用分治法找序列最小值 給一個長度為 $N$ 的整數序列,找出最小值 如何套用分治 ? ---- ## 套用分治法找序列最小值 分(divide):將大問題分成小問題 把當前序列分成左右兩半 每半找最小值 直到序列大小變成只有一個元素 ---- ## 套用分治法找序列最小值 治(conquer):遞迴計算出小問題的答案 左半右半分別遞迴找最小值 ---- ## 套用分治法找序列最小值 合(merge):將小問題的答案合併成大問題的答案 當前序列的最小值為兩半分別的最小值中,比較小的 ---- ## 套用分治法找序列最小值 ```cpp= int arr[100005]; int divide(int l, int r){ if(l == r) return arr[l]; // 分 int m = (l+r)/2; // 治 int x = divide(l, m); int y = divide(m+1, r); // 合 int ret = min(x, y); return ret; } ``` 複雜度是多少呢 ? ---- ## 套用分治法找序列最小值 ```cpp= int arr[100005]; int divide(int l, int r){ if(l == r) return arr[l]; // 分 int m = (l+r)/2; // 治 int x = divide(l, m); int y = divide(m+1, r); // 合 int ret = min(x, y); return ret; } ``` 每次分兩半: O(1) 遞迴下去: O(1) 合併答案: O(1) ---- 會發現每個子問題內都是 O(1) 複雜度 因此複雜度為所有子問題數量的總和 一開始原問題分成兩半 兩半再各自分成兩半 最後一層總共有 n 個子問題 $1+2+4+...+n\to n+\frac{n}{2}+\frac{n}{4}+\frac{n}{8}...=2n$ ---- $1+2+4+...+n\to n+\frac{n}{2}+\frac{n}{4}+\frac{n}{8}...=2n$ 因此複雜度為 $O(N)$ ---- ### Tiling Problem 給定一個 $2^{n}\times 2^{n}$ 的方格,其中有一格已經填滿了 你要使用很多 L 型的方塊填滿方格,求出一種符合的解 ![image](https://hackmd.io/_uploads/HyGM8HiVT.png =400x) ---- 考慮分治 ? 既然是二維的,那嘗試看看分四半? 左上、左下、右上、右下 ![image](https://hackmd.io/_uploads/HJ2PDBo46.png =400x) 會發現分的四半中只有其中一半有填滿一格,因此我們可以放一個方塊 讓其他三塊也恰好填滿一格 ---- ![image](https://hackmd.io/_uploads/HklNOSsN6.png =400x) 如此一來會發現,每半都有一格被填滿, 對於每半又會變成原本的問題 ---- 給定 $2^{n-1}\times 2^{n-1}$ 的 的方格,其中有一格已經填滿了 你要使用很多 L 型的方塊填滿方格,求出一種符合的解 ![image](https://hackmd.io/_uploads/Sk3xk8jVT.png =100x) ![image](https://hackmd.io/_uploads/r1KTRSsEa.png =100x) ![image](https://hackmd.io/_uploads/By9SkIiEa.png =100x) ![image](https://hackmd.io/_uploads/SJPFyLiN6.png =100x) 然後繼續分四半,遞迴求解... ---- ![image](https://hackmd.io/_uploads/SkBoqrs4a.png =400x) ---- ![image](https://hackmd.io/_uploads/SkCr3SiNa.png =400x) 遞迴到 $2\times 2$ 時會發現,每個 $2\times 2$ 的區域會剛好 - 一個已填滿的格子 - 一個 L 型的區域未填滿 ---- 此時就是遞迴的中止條件,至於複雜度怎麼算呢? ---- 第一層 1 個 $2^n\times 2^n$ 第二層 4 個 $2^{n-1}\times 2^{n-1}$ 第三層 $4^2$ 個 $2^{n-2}\times 2^{n-2}$ ... 總共 $n$ 層 對於每層都花 O(1) 的時間放 L 型方塊 複雜度為 $1 + 4 + 16 + ... +4^n = O(4^n)$ ---- ### 分治法 v.s. DP DP 屬於分治法的一種,而他們之間的差別在於 DP 會將子狀態的答案用空間儲存下來,以在重複呼叫時不用重複計算。而一般的分治法子問題不會被重複呼叫,因此不需要記錄所有答案。 ![](https://i.imgur.com/QubMnjg.png =x400) ![](https://i.imgur.com/1SSx5JF.png =x400) --- ## 分治法的複雜度 分治法的複雜度通常能寫成遞迴函數 可用數學方式解出遞迴函數的一般項以得到複雜度(但很難很麻煩) ---- ### 遞迴樹法(Recursion-Tree Method) 把遞迴的狀態畫出來會發現長的就是一棵樹, 根節點為一開始的原問題,往下子節點為遞迴下去的子問題 把整棵遞迴樹畫出來後計算時間總和 ![](https://i.imgur.com/QubMnjg.png =x400) ---- ![](https://i.imgur.com/QubMnjg.png =x400) 第一層為 1 個長度為 N 的問題,$O(N)$ 第二層為 2 個長度為 $\frac{N}{2}$ 的問題,$O(\frac{N}{2})\times 2$ 第三層為 4 個長度為 $\frac{N}{4}$ 的問題,$O(\frac{N}{4})\times 4$ ... 總共 $O(\log N)$ 層 $T(N) = O(N) + O(\frac{N}{2})\times 2 + O(\frac{N}{4})\times 4$... $= O(N\log N)$ ---- ### 主定理(Master Method) 大多情況子問題是大問題的分數倍,且遞迴之外的部分為多項式複雜度: $T(n)=aT(\frac{n}{b})+O(n^\alpha\log^kn)$,令$c=\log_ba$ 1. 若 $c>\alpha$ 則複雜度為 $T(n)=O(n^c)=O(n^{\log_ba})$ 2. 若 $\alpha>c$ 則複雜度為 $T(n)=O(n^\alpha\log^kn)$ 3. 若 $c=\alpha$ 則複雜度為 $T(n)=O(n^\alpha\log^{k+1}n)$ ---- ### 常見遞迴函數複雜度 | 遞迴式 | 時間複雜度 | 範例 | | -------- | -------- | ----- | | $T(n) = T(\frac{n}{b})+O(1)$ | $O(\log N)$ | 遞迴的二分搜 | | $T(n) = 2 T(\frac{n}{2})+O(1)$ | $O(N)$ | 序列找最大值 | | $T(n) = 2 T(\frac{n}{2})+O(N)$ | $O(N\log N)$ | Merge Sort | | $T(n) = 2 T(\frac{n}{2})+O(N\log N)$ | $O(N\log^2 N)$ | 使用 std::sort 合併的 Merge Sort 作法 ---- ### 其餘情況 - 如果只有一個子問題很好計算(與迴圈類似) - 如果子問題的大小與大問題成線性關係($n-k$)則是指數複雜度 - 剩下的自己想通靈解遞迴函數 --- ## 範例 ---- ### 合併排序(merge sort) - 問題:將一個長度為 $n$ 的陣列排序 - 分:將此陣列切半,分成左半邊與右半邊 - 治:遞迴分別將左半邊與右半邊排序 - 合:將兩個已排序好的陣列合併成一個有序陣列 - 複雜度:$T(n)=2T(\frac{n}{2})+O(n)=O(n\log n)$ ---- ### 合併排序(merge sort) - 問題:將一個長度為$n$的陣列排序 - 分:將此陣列切半,分成左半邊與右半邊 - 治:遞迴分別將左半邊與右半邊排序 - 合:將兩個已排序好的陣列合併成一個有序陣列 ---- ### 合併排序(merge sort) 合:將兩個已排序好的陣列合併成一個有序陣列 使用內建函式 std::sort 每次複雜度為 $O(N\log N)$ $T(n) = 2 T(\frac{n}{2})+O(N\log N) = O(N\log^2 N)$ ---- ### 合併排序(merge sort) 實際上合併兩個排序好的序列只需要 $O(N)$ 使用雙指針分別指向兩半的開頭,每次把比較小的放進陣列裡 $O(N\log N)\to O(N)$ ---- ### 合併排序(merge sort) 複雜度即可降低 ! $T(n) = 2 T(\frac{n}{2})+O(N) = O(N\log N)$ ---- ### 快速排序(quick sort) - 問題:將一個長度為 $n$ 的陣列排序 - 分:找一個 pivot,將陣列分成小於/大於 pivot 兩類(小於的丟左邊,大於的丟右邊) - 治:遞迴分別將分出來的兩段排序 - 合:將兩陣列合併,由於左右邊皆有序,且右邊大於左邊,因此不用做動作 - 平均複雜度:$T(n)=2T(\frac{n}{2})+O(n)=O(n\log n)$ - 最差複雜度:$T(n)=T(n-1)+O(n)=O(n^2)$ ---- ## 逆序數對 - 問題:給你一個陣列,求有多少組 $(i,j)$ 符合 $i<j, a_i>a_j$,也就是前項大於後項(此問題等價 bubble sort 最少交換次數) input ``` 5 2 1 5 4 2 ``` output ``` 4 ``` ---- 考慮分治法 分: 將序列分成前半與後半 治: 遞迴分別算出前半與後半的逆序數對 合: 計算橫跨兩半邊的逆序數對數量,也就是 $i$ 在左半邊,$j$ 在右半邊 - 對於左半邊的每一項,右半邊有多少項比它小 ---- - 簡單的作法: - 將右半邊排序就可以用 lower_bound 計算,$O(n\log n)$ - 難一點點的作法: - 將兩半邊分別排序就可以用雙指針計算,然而瓶頸在排序,$O(n\log n)$ ---- 回想 merge sort,既然你都遞迴分別處理了左半邊與右半邊, 不如用 merge sort 在計算完後順便把它排序好, 那麼就不需要重新排序了,$O(n)$ - 複雜度:$T(n)=2T(\frac{n}{2})+O(n)=O(n\log n)$ ---- ## 最大連續和 給一長度為 n 的陣列,詢問陣列中的最大連續元素總和 - $1\le n\le 2\cdot 10^5$ input ``` 5 1 1 -3 4 5 ``` output ``` 9 ``` ---- 考慮分治法 分: 將陣列分成前半與後半 治: 遞迴分別算出前半與後半各自範圍內的最大連續和 合: 計算橫跨兩半邊的最大連續和 - 從切的位置往左找最大後綴和,往右找最大前綴和 加起來即為橫跨區間的最大連續和 ---- ### 最近點對 - 問題:座標平面上給你 $n$ 個點,求最近兩個點的距離 ---- - 分治:先把 $n$ 個點用 $x$ 排序,再分成左右兩半邊 - 遞迴分別找出左右兩半的最近距離,剩下就是兩邊各選一點的情況 - 假設遞迴出來的最近距離是 $d$,很直觀的可以知道只需考慮分割線左右各 $d$ 距離內的點 - 窮舉範圍內的點計算最近距離,最差仍是$O(n^2)$ ---- - 經由[證明](https://oi-wiki.org/geometry/nearest-points/)可以發現,若將這些點以 $y$ 排序,那麼每個點只需要考慮後3個點而已 - 因此可以做到 $O(n\log n)$ 合併,若用 merge sort 的概念,在遞迴時順便對 $y$ 排序,那麼就能做到$O(n)合併$ - 複雜度:$T(n)=2T(\frac{n}{2})+O(n)=O(n\log n)$ </br> <font color="#272727"><small>然而這個問題唬爛才是正解</small></font> --- ### 分治困難點 - 想到可以用分治解 - 合併通常是最難的,分與治通常都很制式 ---- ### 優化 - 由於分治法使用遞迴,因此常數偏大 - 如果仔細去解遞迴函數會發現主定理省掉的常係數其實很大 - 因此一個常見的常數優化方法是:當遞迴到 $n$ 很小時改用暴力計算 - 例如最近點對遞迴到 $n<10$ 時改用暴力計算 - 例如 merge sort 遞迴到 $n<50$ 時改用 insertion sort ---- ### 總結 今天的內容為基礎的分治,之後還有很多很難的應用, 如計算符合的區間數量、樹上分治、圖上分治等等。 分治可能對於初學者來說是一個很大的檻, 但熟悉後對於你的基礎功、計算遞迴複雜度, 都會有很大的進步、以及往後的一些算法 (線段樹、CDQ 分治、DP優化) 也是建立在分治之上 而分治往往也能在很難的題目應用,如果熟悉的話也能應用在很多題目上 往往可以在比賽中為你找到另一條出路。 ---- ### Question Time and Practice [Problem Set](https://vjudge.net/contest/593627)
{"title":"Divide & Conquer","description":"Introduction to Competitive Programming","contributors":"[{\"id\":\"19f09ccf-6b99-452f-971f-955cfc1657f3\",\"add\":6812,\"del\":124}]"}
    912 views
   Owned this note