1. 程式人生 > >分治算法學習

分治算法學習

獨立 instance 分割 ++i 中間 -i mod 歸並 特定

1. 遞歸與分治

1.1 遞歸

遞去,歸來。

  • 能夠用遞歸解決的問題需要滿足三個條件:

    • 原問題可以轉換為一個或多個子問題來求解,而這些子問題的求解方法和原問題完全相同,只是規模不同;

    • 遞歸調用次數必須是有限的;

    • 必須有結束遞歸的條件(遞歸出口)來終止遞歸。

  • 何時使用遞歸:

    • 定義是遞歸的(斐波那契);

    • 數據結構是遞歸的(二叉樹、鏈表);

    • 問題求解的方法是遞歸的。

  • 遞歸轉非遞歸:

    • 通常,尾遞歸可以轉換為等價的非遞歸算法。

    • 對於非尾遞歸的情況,可以在理解遞歸的實現的基礎上 使用棧來模擬

      (二叉樹非遞歸遍歷)。

  • 設計遞歸算法:

    • 先求解問題的遞歸模型。

    • 在設計遞歸算法的時候,如果糾結遞歸樹的每一個階段的話,就會極為復雜。因此,只考慮遞歸樹中的第一層和第二層之間的關系即可,即“大問題” 和 “小問題” 的關系,其他關系類似。

    • 求解問題的遞歸模型:

      • 對原問題 f(n) 進行分析,假設出合理的小問題 f(n-1)

      • 假設小問題 f(n-1) 是可解的,在此基礎上確定大問題 f(n) 的解,即給出 f(n)f(n-1) 之間的關系,也就是確定了遞歸體。(與數學歸納法中確定 i=n-1

        時成立,再求證 i=n 時等式成立的過程相似)

      • 確定一個特定情況(如 f(0)f(1))的解,由此作為遞歸出口。(與數學歸納法中求證 i=0i=1 時等式成立相似)

    • 如,求數組中最小值:

      #可轉換為:當前元素 和 它前面所有元素中的最小值 相比較,這是一個遞歸的問題。
       def Min(arr:list):
           if len(arr) <= 1: #遞歸出口:只含有一個元素,即認為最小(特定問題的解)
               return arr[0]
           m = Min(arr[:-1]) #假設f(n-1)是可解的,即求出來了arr[:-1]中的最小值
           if m < arr[-1]:   #然後將最後一個元素與“假設存在的”最小值比較
               return m
           else:
               return arr[-1]
      

        

1.2 分治法

分治法就是:把一個復雜的問題 分解成兩個或更多的相同或相似的子問題,再把子問題分成更小的子問題……直到最後子問題可以簡單的 直接求解 (各個擊破),原問題的解即 子問題的解的合並

  • 因此分治法的步驟為:

    • 第一步 分: 將原來復雜的問題分解為若幹個規模較小、相互獨立、與原問題形式相同的子問題,分解到可以直接求解為止。

    • 第二步 治: 此時可以直接求解。

    • 第三步 合: 將小規模的問題的解合並為一個更大規模的問題的解,自底向上 逐步求出原來問題的解。

  • 算法的設計模式:

     Divide_and_Conquer(P){
         if(xxx) //遞歸出口:如果規模足夠小,克制直接求解,則開始“治”
             return ADHOC(P); //ADHOC是治理可直接求解子問題的子過程
         
         <divide P into smaller subinstances P1,P2,...Pk>; //將P“分”解為k個子問題
         
         for(int i = 0; i < k; ++i)
             yi = Divide_and_Conquer(Pi); //遞歸求解各個子問題
         
         return merge(y1, y2, ..., yk); //將各個子問題的解“合”並為原問題的解
     }
    

      

  • 設計劃分策略,把原問題P分解成K個規模較小的子問題,這個步驟是分支算法的基礎和關鍵。需要遵循兩個原則:

    • 平衡子問題原則:分解出的k個子問題規模最好大致相當;

    • 獨立子問題原則:分解出的k個子問題之間重疊越少越好,最好k個子問題相互獨立,不存在重疊子問題。

  • 分治法的復雜度分析(主定理):

    • 有遞推關系式 T(n) = aT(n/b) + f(n),其中n為問題規模,a為遞推的子問題數量,n/b 為子問題的規模(假設每個子問題的規模都一樣),f(n) 為遞推以外進行的計算工作, a>=1, b>=1 是常數。則:

      • 若對於常數ε >0,有 f(n) = O(nlogb(a)-ε) ,ε > 0,則 T(n) = Θ(nlogba) ;(O(xxx)是f(n)的上界,T(n)的漸進階為較大的那個,較大的就是 nlogba,因此T(n)就等於它)

      • f(n) = θ(nlogba) ,則 T(n) = Θ(nlogba·logn) (小θ表示同階)

      • 若常數 ε > 0,有 f(n) = Ω(nlogb(a)+ε) ,且對於某常數 c > 1 和所有充分大的額正整數n有 af(n/b) <= cf(n),則 T(n) = Θ(f(n)) 。(Ω(xxx)是f(n)的下界,T(n)的漸進階為較大的那個,較大的就是f(n),因此T(n)就等於它)

    • 例1:

      • 求 T(n) = 9T(n/3) + n 漸進階(Θ) :

      • a = 9, b = 3, f(n) = n; n^(logba) = n^2; 取 ε = 0.1 ,f(n) = O(nlogb(a) - ε) = O(n^1.9),因此 T(n) = Θ(n^2)

    • 例2:

      • T(n) = T(2n/3) + 1

      • a = 1, b = 3/2, f(n) = 1, n^(logba) = n^0 = 1, f(n) = 1, f(n) 與 n^(logba) 同階,則 T(n) = Θ(logn)

    • 例3:

      • T(n) = 2T(n/2) + nlogn

      • a = 2, b= 2, f(n) = nlogn, n^(logba) = n^1 = n, f(n) = nlogn,n^(logba) 是 f(n) 的下限,但不滿足 f(n) = Ω(nlogb(a)+ε) ,因此無法應用Master定理。

  • 例:分治 - 求和

     
    def sum(arr:list):
         if len(arr) == 1:
             return arr #“治” 直接求解
         mid = len(arr)//2
         l = arr[:mid]
         r = arr[mid:] #“分” 分解成兩個子問題
         return sum(l) + sum(r) #“合” 自底向上求子數組l和r的和
     ?
     sum([1,2,3,4]) #10
    

      技術分享圖片

    技術分享圖片

  • 例:分治 - 歸並排序

    對於一個含有很多元素的數組,直接排序不容易,但如果分解成多個規模很小的數組(只有一個元素),那麽這時候兩個子數組排序(歸並)就很容易了。

     
    def merge(arr:list, low:int, mid:int, high:int):
         l = arr[low:mid]
         r = arr[mid:high]
         i = j = 0 # 用於叠代子序列l、r
         k = low
         len_l = mid-low
         len_r = high-mid
         while i < len_l or j < len_r:
             if j == len_r or (i < len_l and l[i] <= r[j]): 
                 arr[k] = l[i]
                 k += 1
                 i += 1
             else:
                 arr[k] = r[j]
                 k += 1
                 j += 1
     ?
     def mergeSort(arr:list, low:int, high:int):
         if high-low == 1: #一個元素本身已經有序
             return arr #“治” 直接求解
         mid = (high+low) // 2
         mergeSort(arr, low, mid)
         mergeSort(arr, mid, high) #“分” 分解成兩個子問題
         merge(arr, low, mid, high) #“合” 自底向上歸並兩個有序子數組
         
     arr = [3,1,2,4,5]
     mergeSort(arr, 0, len(arr)) #[1,2,3,4,5]
    

      技術分享圖片

    技術分享圖片

  • 例:分治 - 求逆序對數目

    • 如,{3, 1, 2, 4} 中逆序對有 <3,1> <3,2>

    • 如果枚舉的話,時間復雜度是O(n^2)。可以借用歸並排序(O(nlogn))。

      • 如果數組只有一個元素,逆序對為0;

      • 原數組A中逆序對可以分為三個部分:第一部分為子數組A1中的逆序對數C1;第二部分為A2中的C2;第三部分是一個元素在A1,而另一個元素在A2,這兩個元素構成一個逆序對,C3。

      • 對於C3,可以借助歸並的過程判斷。

     def merge(arr:list, low:int, mid:int, high:int):
         num = 0 #逆序對數
         l = arr[low:mid]
         r = arr[mid:high]
         i = j = 0 # 用於叠代子序列l、r
         k = low
         len_l = mid-low
         len_r = high-mid
         while i < len_l or j < len_r:
             if j == len_r or (i < len_l and l[i] <= r[j]): 
                 arr[k] = l[i]
                 k, i = k + 1, i + 1
             else:
                 arr[k] = r[j]
                 k, j = k + 1, j + 1
                 if i != len_l: #判斷是因為,當i==len_l時,就會多加
                     num += 1 #【如果 l[i] < r[j] 則存在逆序對】
          return num       
     ?
     def mergeSort(arr:list, low:int, high:int):
         if high-low == 1: #一個元素本身已經有序
             return 0 #“治” 直接求解,只含有一個元素的數組的逆序對的數目為0
         mid = (high+low) // 2
         C1 = mergeSort(arr, low, mid)
         C2 = mergeSort(arr, mid, high) #“分” 分解成兩個子問題
         C3 = merge(arr, low, mid, high) #“合” 自底向上歸並兩個有序子數組
         return C1 + C2 + C3
         
     arr = [3,1,2,4,5]
     mergeSort(arr, 0, len(arr)) #3
    

      技術分享圖片

技術分享圖片

  • 例:分治 - 快速排序

    思想:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。

    • 過程:

      • 分: 對於一個整數集合 A = {a[l], ..., a[r]} ,以 a[l] 為基準將A劃分為三個部分:A1、A2、A3,其中 A2 = {a[l]} ,A1所有元素都小於等於A2,A3所有元素都小於A1。然後分別對A1、A3進行遞歸處理,從而使得整個集合有序。

      • 治: 如果劃分後的A1和A3只含有一個元素,則已經有序,直接返回即可。

      • 合: 因為在分的過程中A1和A3已經排好序了,所以A1、A2和A3已經有序了,在合並階段無需處理。

     def partition(arr:list, low:int, high:int):
         base = arr[low] # 備份基準元素,現在arr[low]空出來了
         while low < high:
             while (low < high) and (arr[high] > base): #從右至左找到一個比基準元素小的
                 high -= 1
             arr[low] = arr[high] #arr[low]已經空出,將比基準元素小的arr[high]填入空出的位置,然後arr[high]空出
             while (low < high) and (arr[low] <= base): #從左至右找到一個比基準元素大的
                 low += 1
             arr[high] = arr[low] #arr[high]已經空出,將比基準元素大的arr[low]填入空出的位置,然後arr[low]空出
         arr[low] = base #while終止時 high==low,最後終止的位置,一定是high或low空出的(因為內while終止時,是被另一個位置拿走一個值,然後空出),所以可以直接將基準元素填入
         return high 
     ?
     def quickSort(arr:list, low:int, high:int):
         if low < high:
             k = partition(arr, low, high)
             quickSort(arr, low, k - 1)
             quickSort(arr, k + 1, high)
             
     ?
     ### === 其他劃分思路 ===
     ### 雙路快排【困擾了半個月】
     def partition(arr, l, r):
         base = arr[l]
         ll = l
         #l += 1 #不能加!加了之後只有兩個元素的情況無法進入循環。如以下兩種:
                 # [1,2]
                 # [2,1]
         while l < r:
             while l < r and arr[r] > base: #必須從右端開始,因為只有這樣,循環因為l<r的條件不符合時終止的位置的元素才能比base小。如果先從左端開始的話,最後l==r,r此時指向的元素極有可能大於base,因此最後交換的話,比base大的值會放到base的左邊。如[3,4,2,1,5]畫圖可看出。
                 r -= 1
             while l < r and arr[l] <= base:
                 l += 1
             arr[l], arr[r] = arr[r], arr[l]
         arr[ll], arr[r] = arr[r], arr[ll]
         return r
     ?
     ?
     ### ll+1 ~ l 是小於base的區域
     def pt(arr, l, r):
         ll = l
         base = arr[l]
         for i in range(ll+1, r+1):
             if arr[i] < base:
                 l += 1
                 arr[l], arr[i] = arr[i], arr[l]
         arr[ll], arr[l] = arr[l], arr[ll]
         return l
    

      技術分享圖片(圖:學堂在線鄧俊輝的老師MOOC )

    技術分享圖片

    • 性能分析:

      • 不穩定排序:因為low和high移動方向相反,左右側的相同大小的元素可能顛倒。

      • 就地(空間復雜度O(1))。

      • 最好情況時間復雜度:每次劃分都(接近)平均,軸點總是(接近)中點;復雜度為 T(n) = 2·T((n-1)/2) + O(n) = O(nlogn) (減一是中間的軸點沒包括)。

      • 最壞情況:每次劃分都極不均衡,復雜度為 T(n) = T(n-1) + T(0) + O(n) = O(n^2)

      • 平均情況:O(nlogn)

分治算法學習