演算法設計範式

分治法

分治法是一套三拍子的策略:把問題劃分(divide)成同類的更小片段,征服(conquer)每個片段(通常靠遞迴),再把各片段的答案合併(combine)成整體的答案。其直覺是:一個你一眼看不透的大問題,往往能拆成兩半,每一半都只是原問題的縮小版——而一旦兩半都解決了,把它們縫起來就很容易。它是遞迴天生的搭檔:「征服」這一步幾乎總是對更小輸入的一次遞迴呼叫,而基準情形就是小到可以直接求解的那一片。

兩個經典例子能說明它的形狀。合併排序把陣列劃分成兩半,遞迴地各自排好序,再把兩段已排序的部分合併成一個有序的整體——這個合併正是巧妙的「合併」步。二分搜尋把搜尋區間對半劃分,扔掉不可能含目標的那一半,對可能含目標的那一半遞迴;這裡根本沒有合併步,只有不留情面的對半砍。兩者的速度都源自同一處:每一層劃分都把工作量砍小,所以層數大約只有 log n。

分治演算法的代價由一個遞迴關係刻畫——例如合併排序的 T(n) = 2T(n/2) + O(n),讀作「要排好 n 個元素,先排兩半、再花線性時間合併」。主定理幾乎一眼就能把這類遞迴化成 Big-O 答案(合併排序解出來是 O(n log n))。一句提醒:只有當子問題真的更小、且合併步足夠便宜時,劃分才有幫助。如果合併和從頭解一樣昂貴,你就什麼也沒省下。

void mergeSort(vector<int>& a, int lo, int hi) {
  if (lo >= hi) return;          // base: 0 or 1 element
  int mid = lo + (hi - lo) / 2;
  mergeSort(a, lo, mid);         // divide + conquer left
  mergeSort(a, mid + 1, hi);     // divide + conquer right
  merge(a, lo, mid, hi);         // combine the two sorted halves
}

遞迴 T(n) = 2T(n/2) + O(n) 經主定理給出 O(n log n)。

分治法不同於動態規劃。兩者都把問題拆成子問題,但經典分治的子問題是相互獨立的(合併排序的兩半永遠不重疊),而動態規劃的存在恰恰是為了應對那些會重疊、否則就要被重算的子問題。

又稱
divide-and-conquerD&C分治分而治之分治演算法