zoukankan      html  css  js  c++  java
  • 分治算法学习

    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)

  • 相关阅读:
    贪心法之最优装载问题
    判断回文
    P1217 [USACO1.5]回文质数 Prime Palindromes
    李白打酒
    P1036 选数
    P1028 数的计算
    P1316 丢瓶盖
    P1181 数列分段Section I
    P1182 数列分段`Section II`
    P1216 [IOI1994][USACO1.5]数字三角形 Number Triangles
  • 原文地址:https://www.cnblogs.com/yuanyb/p/10567023.html
Copyright © 2011-2022 走看看