2020-6-28- 卑鄙是卑鄙者的通行證,高尚是高尚者的墓誌銘。
北島 – 《回答》
- 卑鄙是卑鄙者的通行證,高尚是高尚者的墓誌銘。
一、概述
分治算法(divide and conquer)的核心思想其實就是四個字,分而治之 ,也就是將原問題劃分成 n 個規模較小,並且結構與原問題相似的子問題,遞歸地解決這些子問題,然後再合併其結果,就得到原問題的解。
這個定義看起來有點類似遞歸的定義。關於分治和遞歸的區別,分治算法是一種處理問題的思想,遞歸是一種編程技巧。實際上,分治算法一般都比較適合用遞歸來實現。分治算法的遞歸實現中,每一層遞歸都會涉及這樣三個操作:
- 分解:將原問題分解成一系列子問題;
- 解決:遞歸地求解各個子問題,若子問題足夠小,則直接求解;
- 合併:將子問題的結果合併成原問題。
分治算法能解決的問題,一般需要滿足下面這幾個條件:
- 原問題與分解成的小問題具有相同的模式;
- 原問題分解成的子問題可以獨立求解,子問題之間沒有相關性,這一點是分治算法跟動態規劃的明顯區別,等我們講到動態規劃的時候,會詳細對比這兩種算法;
- 具有分解終止條件,也就是說,當問題足夠小時,可以直接求解;
- 可以將子問題合併成原問題,而這個合併操作的複雜度不能太高,否則就起不到減小算法總體複雜度的效果了。
二、應用舉例分析
2.1、分治算法求解數據的有序度
在排序算法裏有數據的有序度、逆序度的概念,我們用有序度來表示一組數據的有序程度,用逆序度表示一組數據的無序程度。
假設我們有 n 個數據,我們期望數據從小到大排列,那完全有序的數據的有序度就是 n(n-1)/2,逆序度等於 0;相反,倒序排列的數據的有序度就是 0,逆序度是 n(n-1)/2。除了這兩種極端情況外,我們通過計算有序對或者逆序對的個數,來表示數據的有序度或逆序度。
現在的問題是,如何編程求出一組數據的有序對個數或者逆序對個數呢?因爲有序對個數和逆序對個數的求解方式是類似的,所以你可以只思考逆序對個數的求解方法。
最笨的方法是,拿每個數字跟它後面的數字比較,看有幾個比它小的。我們把比它小的數字個數記作 k,通過這樣的方式,把每個數字都考察一遍之後,然後對每個數字對應的 k 值求和,最後得到的總和就是逆序對個數。不過,這樣操作的時間複雜度是 O(n^2)。那有沒有更加高效的處理方法呢?
我們用分治算法來試試。我們套用分治的思想來求數組 A 的逆序對個數。我們可以將數組分成前後兩半 A1 和 A2,分別計算 A1 和 A2 的逆序對個數 K1 和 K2,然後再計算 A1 與 A2 之間的逆序對個數 K3。那數組 A 的逆序對個數就等於 K1+K2+K3。
我們前面講過,使用分治算法其中一個要求是,子問題合併的代價不能太大,否則就起不了降低時間複雜度的效果了。那回到這個問題,如何快速計算出兩個子問題 A1 與 A2 之間的逆序對個數呢?
這裏就要藉助歸併排序算法了。你可以先試着想想,如何藉助歸併排序算法來解決呢?
歸併排序中有一個非常關鍵的操作,就是將兩個有序的小數組,合併成一個有序的數組。實際上,在這個合併的過程中,我們就可以計算這兩個小數組的逆序對個數了。每次合併操作,我們都計算逆序對個數,把這些計算出來的逆序對個數求和,就是這個數組的逆序對個數了。
把這個過程翻譯成了代碼,你可以結合着圖和文字描述一起看下。
private int num = 0; // 全局變量或者成員變量
public int count(int[] a, int n) {
num = 0;
mergeSortCounting(a, 0, n-1);
return num;
}
private void mergeSortCounting(int[] a, int p, int r) {
if (p >= r) return;
int q = (p+r)/2;
mergeSortCounting(a, p, q);
mergeSortCounting(a, q+1, r);
merge(a, p, q, r);
}
private void merge(int[] a, int p, int q, int r) {
int i = p, j = q+1, k = 0;
int[] tmp = new int[r-p+1];
while (i<=q && j<=r) {
if (a[i] <= a[j]) {
tmp[k++] = a[i++];
} else {
num += (q-i+1); // 統計 p-q 之間,比 a[j] 大的元素個數
tmp[k++] = a[j++];
}
}
while (i <= q) { // 處理剩下的
tmp[k++] = a[i++];
}
while (j <= r) { // 處理剩下的
tmp[k++] = a[j++];
}
for (i = 0; i <= r-p; ++i) { // 從 tmp 拷貝回 a
a[p+i] = tmp[i];
}
有很多同學經常說,某某算法思想如此巧妙,我是怎麼也想不到的。實際上,確實是的。有些算法確實非常巧妙,並不是每個人短時間都能想到的。比如這個問題,並不是每個人都能想到可以藉助歸併排序算法來解決,不誇張地說,如果之前沒接觸過,絕大部分人都想不到。但是,如果我告訴你可以藉助歸併排序算法來解決,那你就應該要想到如何改造歸併排序,來求解這個問題了,只要你能做到這一點,我覺得就很棒了。
2.2、練習
-
二維平面上有 n 個點,如何快速計算出兩個距離最近的點對?
-
有兩個 nn 的矩陣 A,B,如何快速求解兩個矩陣的乘積 C=AB?
三、參考資料
- 王爭 – 《極客時間|數據結構與算法之美》