分治算法

1. 分治算法

分治算法的核心就是分而治之,也就是將原問題劃分爲若干個規模更小但結構與原問題相似的子問題,遞歸地解決這些子問題然後進行合併,就可以得到原問題的解。比如歸併排序就是將原數據劃分爲左右兩個部分,然後分別遞歸對左右兩部分排序,排完序後再合併兩個有序區間數據即可得到最終整體有序的數據。

分治算法能解決的問題,一般需要滿足以下幾個條件:

  • 原問題與分解後的子問題求解方法相同
  • 各個子問題可以獨立求解,互相之間不依賴
  • 具有分解終止條件,也就是問題足夠小時可以直接求解
  • 子問題的合併複雜度不能太高,否則就不能起到減小時間複雜度的作用了

2. 分治算法實戰分析

2.1. 逆序數對

數據中逆序元素對的定義爲:a[i]>a[j], i<ja[i] > a[j], 如果 \space i < j。比如 2,4,3,1,5,6 這組數據的逆序元素對就有 (2, 1),(4, 3),(4, 1),(3, 1) 共計 4 個。

怎麼統計一組數據中的逆序對數呢?最簡單的方法就是針對數組中的每一個元素,查看其後面有多少個大於其的數據。但是,這樣操作的時間複雜度是 O(n2)O(n^2),有沒有更快的方法呢?

藉助於分治算法,我們可以將數據劃分爲左右兩部分 L 和 R,分別計算 L 和 R 中的逆序數對,然後再計算 L 和 R 之間的逆序數對,便可以得到整體數據的逆序數對了。這時候,我們可以想到歸併排序的合併操作是對左右兩個有序數據進行合併,那麼左右兩部分數據的逆序數對就爲零,我們只需要在合併的時候統計左半部分和右半部分之間的逆序數對即可。

代碼如下:

int num = 0;

void Merge_Sort(float data[], int left, int right, float sorted_data[])
{
    if(left < right)
    {
        int mid = (left + right) / 2;
        Merge_Sort(data, left, mid, sorted_data);
        Merge_Sort(data, mid+1, right, sorted_data);
        Merge_Array(data, left, mid, right, sorted_data);
    }
}

void Merge_Array(float data[], int left, int mid, int right, float temp[])
{
    int i = left, j = mid + 1;
    int k = 0;

    // 從子數組的頭開始比較
    while(i <= mid && j <= right)
    {
        if (data[i] <= data[j])
        {
            temp[k++] = data[i++];
        }
        else
        {   // [i, mid] 的元素都比 data[j] 大
            num += (mid - i + 1);
            temp[k++] = data[j++];
        }
    }

    // 判斷哪個子數組還有元素,並拷貝到 temp 後面
    while(i <= mid)
    {
        temp[k++] = data[i++];
    }
    while(j <= right)
    {
        temp[k++] = data[j++];
    }

    // 將 temp 中的數據拷貝到原數組對應位置
    for(i = 0; i < k; i++)
    {
        data[left+i] = temp[i];
    }
}

2.2. 矩陣乘法

假設我們有兩個 n×nn×n 的矩陣 A 和 B,矩陣相乘後得到的矩陣 C 也是 n×nn×n 的,其中每個位置的元素是一行乘以一列,需要 nn 次乘加操作,所以總的時間複雜度爲 O(n2n)=O(n3)O(n^2*n)=O(n^3)

藉助於分治算法,我們可以先將原始矩陣分解爲 4 個 n/2×n/2n/2×n/2 的小矩陣,

然後分別計算 n/2×n/2n/2×n/2 小矩陣的 10 個加減和 7 個乘法,

最後再計算 n/2×n/2n/2×n/2 小矩陣的 8 個加減即可得到輸出矩陣。

其中,小矩陣的加減需要 O(n2)O(n^2),所以時間複雜度可以表示爲:

T(n)=7T(n/2)+O(n2)T(n)=7T(n/2)+O(n^2)

利用 Master 定理可得 T(n)=O(nlog7)O(n2.81)T(n)=O(n^{log7}) \approx O(n^{2.81})

2.3. 最近點對

針對一維空間,我們先找到所有點座標的中位數,然後將點對分爲兩部分,遞歸求出左右兩邊的最近點對 (p1,p2)(p_1, p_2)(q1,q2)(q_1, q_2),然後再找出左半部分最右邊的點和右半部分最左邊的點組成的點對 (p3,q3)(p_3, q_3),那麼最近點對一定是這三個中的一個。

其中尋找中位數、找到左半部分最右邊點以及右半部分最左邊點需要的時間複雜度都爲 O(n)O(n),所以有:

T(n)=2T(n/2)+O(n)T(n)=O(nlogn)T(n)=2T(n/2)+O(n) \to T(n)=O(nlogn)

針對二維空間,我們需要先找到所有點橫座標的中位數,然後將點對分爲兩部分,同樣地,我們遞歸找到 (p1,p2)(p_1, p_2)(q1,q2)(q_1, q_2),然後再找出臨界區中距離最小的點對 (p3,q3)(p_3, q_3),這三者距離最小的就是所求。

重點就在於怎麼找到 (p3,q3)(p_3, q_3)。如果我們直接遍歷左半部分所有的點,求出它們和右半部分每一個點的距離,那麼時間複雜度爲 O(n2)O(n^2)。我們就有:

T(n)=2T(n/2)+O(n2)T(n)=O(n2)T(n)=2T(n/2)+O(n^2) \to T(n)=O(n^2)

這顯然是我們不滿意的。怎麼才能降低合併過程的時間複雜度呢?

假設我們遞歸得到的左右兩邊最近點對的最小距離爲 d,對於左邊的一個點 p,我們沒必要遍歷右邊所有的 q。

如上圖所示,如果 p 和 q 的距離小於 d,那麼 q 只能位於 p-右鄰域,而當點 p 位於中線上時,右鄰域最大,包含的可能的點 q 才最多。在這種情況下,可能的 q 點有 6 個,分別位於 6 個長方形區域。如果點數多餘 6 個,那麼必然有兩個點位於同一個長方形區域,那麼這兩個點的距離最大爲 5d/6<d5d/6<d,這是不可能的,因爲 d 是左右兩邊的最近點對距離。

那麼我們怎麼找到這些點呢?我們需要維護一個按照橫座標排序的點對 XX 和一個按照縱座標排序的點對 YY,排序的時間複雜度爲 O(nlogn)O(nlogn)。分治的時候,我們利用 XX 來找到橫座標的中位數 x=mx=m,然後遍歷 YY 通過橫座標的比較來進行劃分。合併的時候,我們通過判斷橫座標與分界線距離小於 dd 找到位於臨界區的點集 SS,此時它們還是縱座標有序的。這樣我們遍歷 SS 中的每個點,然後向後查找與其縱座標距離小於 dd 的點計算距離(這些點的個數是有限的正如我們上面的分析那樣),看是否有小於 dd 的點對即可。所以分治和合並的時間複雜度都可以做到 O(n)O(n),算法整體複雜度爲

T(n)=2T(n/2)+O(n)T(n)=O(nlogn)T(n)=2T(n/2)+O(n) \to T(n)=O(nlogn)

參考資料-極客時間專欄《數據結構與算法之美》

獲取更多精彩,請關注「seniusen」!

發佈了214 篇原創文章 · 獲贊 86 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章