分治策略及其应用

一、分治策略简介

在很多情况下,分治策略有可能是简单而强有力的方法,可以用来提高算法的效率

不要问我为什么,他就是这样的……

大概的步骤有三部分

1,将输入的数据分成若干部分

2、递归的求解每个部分的问题

3、将子问题的解组合成为一个全局的解

二、应用领域

分治算法可以被运用到许许多多的方面,可以充当其他算法的一个i组成部分,也可以稍做修改成为另一个算法,例如

归并排序

找平面上最邻近的点对问题

降低整数相乘的复杂度

消除信号的噪音

……

三、算法核心部分分析

马克思和恩格斯教导我们一定要抓住主要矛盾的主要方面

从我的角度看,我们学习分治算法的主要矛盾是将其应用的其他方面

而主要方面就是一定要理解算法的核心部分

这样就可以根据实际情况对分治算法做出适当的修改,提高解决问题的效率

接下来我将会通过解决一下几个问题,让大家比较简单的掌握分治算法

  • 分治算法的整体步骤大概是什么样的

  • 怎么预计和分析一个分治算法的好坏,即对分治算法的复杂度进行评估

  • 怎么改进一个分治算法

上面以及提到过分治算法的大概过程

我再重述一下

  1. 将输入的数据分成若干部分

  2. 递归的求解每个部分的问题

  3. 将子问题的解组合成为一个全局的解

为了让大家更好的理解这个过程就拿最为熟悉的归并排序算法举例


嘛也不说直接上源代码

python实现

# 归并排序主体
def mergeSort(arr):
    # 如果数组中只有两个数
    if(len(arr)<2):
        return arr
    
    middle = math.floor(len(arr)/2)
    
    # 将现有的整个数组分成左右两个部分,采用的切割方法是从middle处切割
    left, right = arr[0:middle], arr[middle:]
    # 最后递归合并
    return merge(mergeSort(left), mergeSort(right))

def merge(left,right):
    # 存储结果
    result = []
    
    # 比较排头的元素
    while left and right:
        if left[0] <= right[0]:
            result.append(left.pop(0))
        else:
            result.append(right.pop(0))
    # 合并左右部分
    while left:
        result.append(left.pop(0))
    while right:
        result.append(right.pop(0))
    
    return result

if __name__ == '__main__':
    list = [1, 5, 8, 45, 98, 90, 78, 99, 123, 2]
    print("List is:", list)
    result = mergeSort(list)
    print("list sorted :", result)

相信很多人又这样的问题,为啥这玩意要在middle处切割的,又为啥要分成两个部分,不分成三个,四个……部分呢?这些问题就是我们下面要详细讨论的了


“小朋友,你是否有很多问好???”


说实话,一开始学的时候我也一脸懵逼━┳━ ━┳━

要想弄懂这个问题就我们就需要知道下面这个公式,并且需要会进行对应的推导


四、公式推导

首先定义T(n)为该算法在规模为n的输入实例的最坏的运行时间

然后看

T ( n ) = { a T ( n / b ) + g ( n ) ( n > 1 ) 0   ( n = 1 ) T\left( n \right) =\left\{ \begin{array}{l} aT\left( n/b \right) +g\left( n \right) \left( n>1 \right)\\ 0\ \left( n=1 \right)\\ \end{array} \right.

这个公式

这个公式就是归并排序的递推公式,下面的很多问题就是围绕这个问题展开的


公式的含义

  • 一个规模为n的问题每一次被分成了a个子问题,

  • 每个问题的规模是n/b

  • 各个子问题的解的复杂度为 g(n)


接下来的这个图你们一定就可以看懂了

在这里插入图片描述

这个图就是上面归并排序的分治策略图


n为2的倍数

接下来我们就手动推导出归并排序的复杂度为 T ( n ) = n log 2 n T\left( n \right) = n\log_{2}n

首先根据

T ( n ) = a T ( n b ) + g ( n ) T\left( n \right) = aT\left( \frac{n}{b} \right) + g\left( n \right)

我们可以写出

归并排序中每一次被分为2个子问题,每一个子问题的规模是其爸爸的一半,因为归并排序进行排序操作的时候只比较头两个元素的大小,当一半为空时,直接把另一半的其他部分加到结果上就行,所以每一个子问题的复杂度为n

首先从简单的n为2的倍数开始

T ( n ) = 2 T ( n 2 ) + n T\left( n \right) = 2T\left( \frac{n}{2} \right) + n

T ( n ) n = 2 T ( n 2 ) n + 1 \frac{T\left( n \right)}{n} = \frac{2T\left( \frac{n}{2} \right)}{n} + 1

= T ( n 2 ) n 2 + 1 = \frac{T\left( \frac{n}{2} \right)}{\frac{n}{2}} + 1

= T ( n 4 ) n 4 + 1 + 1 = \frac{T\left( \frac{n}{4} \right)}{\frac{n}{4}} + 1 + 1

= T ( n 8 ) ( n 8 ) + 1 + 1 + 1 = \frac{T\left( \frac{n}{8} \right)}{\left( \frac{n}{8} \right)} + 1 + 1 + 1

\ldots\ldots

= T ( n n ) ( n n ) + log 2 n 1 = \frac{T\left( \frac{n}{n} \right)}{\left( \frac{n}{n} \right)} + \log_{2}n*1

= log 2 n = \log_{2}n

T ( n ) = n log 2 n T\left( n \right) = n\log_{2}n


n为自然数

其次对于一般情况我们要证明

T ( n ) n   c e i l i n g ( log 2 n ) T\left( n \right) \leq n\ ceiling\left( \log_{2}n \right)

递推公式为
T ( n ) < =   { 0   ( n = 1 ) T ( c e i l i n g ( n / 2 ) ) + T ( f l o o r ( n / 2 ) ) + n   ( n > 1 ) T\left( n \right) <=\ \left\{ \begin{array}{l} 0\ \left( n=1 \right)\\ T\left( ceiling\left( n/2 \right) \right) +T\left( floor\left( n/2 \right) \right) +n\ \left( n>1 \right)\\ \end{array} \right.
我们采用归纳法

当n= 1时显然成立

n 1 = f l o o r ( n 2 )   n 2 = ceiling ( n 2 ) n_{1} = floor\left( \frac{n}{2} \right)\ n_{2} = \text{ceiling}\left( \frac{n}{2} \right)

假设 n 1 n - 1 命题时命题成立

T ( n ) < = T ( n 1 ) + T ( n 2 ) + n T\left( n \right) <=T\left( n_1 \right) +T\left( n_2 \right) +n < = n 1 ( c e i l i n g ( log 2 n 1 ) ) + n 2 ( c e i l i n g ( log 2   n 2 ) ) + n <=n_1\left( ceiling\left( \log _2n_1 \right) \right) +n_2\left( ceiling\left( \log _2\ n_2 \right) \right) +n < = n 1 ( c e i l i n g ( log 2   n 2 ) ) + n 2 ( c e i l i n g ( log 2   n 2 ) ) + n <=n_1\left( ceiling\left( \log _2\ n_2 \right) \right) +n_2\left( ceiling\left( \log _2\ n_2 \right) \right) +n = n ( c e i l i n g ( log 2   n 2 ) ) + n =n\left( ceiling\left( \log _2\ n_2 \right) \right) +n < = n ( c e i l i n g ( log 2   c e i l i n g ( n / 2 ) ) ) + n <=n\left( ceiling\left( \log _2\ ceiling\left( n/2 \right) \right) \right) +n < = n ( c e i l i n g ( log 2   n ) 1 ) + n <=n\left( ceiling\left( \log _2\ n \right) -1 \right) +n < = n ( c e i l i n g ( log 2   n ) ) <=n\left( ceiling\left( \log _2\ n \right) \right)

综上,我们回答刚刚的问题为啥这玩意要在middle处切割的,又为啥要分成两个部分,不分成三个,四个…部分呢?

其实可以在不同地方切割,我们一般选择在middle出切割,也可选择切成多个部分,但是这样的复杂度时不同的


g(n)=c

对于g(n)=c

这个公式最后可以证明

T ( n ) = O ( n log b a )   T\left( n \right) = O\left( n^{\log_{b}a} \right)\


g(n) = c*n^d

对于 g ( n ) = cn d g\left( n \right) = \text{cn}^{d}

满足如下定理

设函数满足这个递推关系 T ( n ) = a T ( n b ) + cn d T\left( n \right) = aT\left( \frac{n}{b} \right) + \text{cn}^{d} 的增函数,其中 n = b k n = b^{k} ,a>=1,b是大于1的正整数,c,d为实数且c>=0,d>=0,那么
T ( n ) = { O ( n d )   a < b d O ( n d log   n )   a = b d O ( n log a   b )   a > b d T\left( n \right) =\left\{ \begin{array}{l} O\left( n^d \right) \ a<b^d\\ O\left( n^d\log\text{\ }n \right) \ a=b^d\\ O\left( n^{\log _a\ b} \right) \ a>b^d\\ \end{array} \right.
有兴趣的可以自己证明一下,这里就不给出证明了

敲公式实在太累人了ε(┬┬﹏┬┬)3

五、分治算法的应用

经过对核心算法的理解

我们就可以解决之前提出的问题了

分治算法第一步就是就是将问题分成对应的a部分,每个问题的规模是n/b之后递归求解每一个问题,最后进行组合

那怎么对一个分治算法的复杂度进行评估呢?

我们可以用上面T(n)公式进行推导,将对应的a,b,g(n),带入然后计算出T(n)

同时我们可以运用不同的计算方法改变a,b,g(n)的值,由此来达到改进算法效率的目的


接下来我们就通过改进整数的乘法来看一下分治算法的牛逼之处吧

最普通的乘法少说一点吧

x*y,在计算机中是通过二进制来计算的,所以是先转为二进制再相乘

复杂度为O(n^2)

接下来我们通过分治算法来进行改进


我们令

x = 2 ( n / 2 ) x 1 + x 0 x = 2^{(n/2)}*x1+x0

y = 2 ( n / 2 ) y 1 + y 0 y = 2^{(n/2)}*y1+y0

x y = ( 2 ( n / 2 ) x 1 + x 0 ) ( 2 ( n / 2 ) y 1 + y 0 ) x*y = (2^{(n/2)}*x1+x0)(2^{(n/2)}*y1+y0)

= 2 n x 1 y 1 + 2 ( n / 2 ) ( x 1 y 0 + x 0 y 1 ) + x 0 y 0 = 2^n *x1* y1+2^{(n/2)}*(x1*y0+x0*y1)+x0*y0

我们可以通过递推公式来看一下,现在的算法优劣情况

T ( n ) = 4 T ( n / 2 ) + O ( n ) T(n) = 4T(n/2) +O(n)

= > T ( n ) = O ( n 2 ) => T(n)=O(n^2)

O(n) 为每次移位和相加的复杂度

貌似没有啥改变……

接下来看看我们可不可以通过改变a,b,O(n)的值来降低复杂度

可以看出O(n),没有啥可以很好的改进的地方

但是我们可以发现

x y = 2 n x 1 y 1 + 2 ( n / 2 ) ( x 1 y 0 + x 0 y 1 ) + x 0 y 0 x*y=2^n *x1* y1+2^{(n/2)}*(x1*y0+x0*y1)+x0*y0

= 2 n x 1 y 1 + 2 ( n / 2 ) ( ( x 1 + x 0 ) ( y 1 + y 0 ) x 1 y 1 x 0 y 0 ) + x 0 y 0 = 2^n *x1*y1+2^{(n/2)}*((x1+x0)*(y1+y0)-x1*y1-x0*y0)+x0*y0

这样就成将4次乘法降低为3次了
让我们再来看一次复杂度

T ( n ) = 3 T ( n / 2 ) + O ( n ) = > T ( n ) = O ( n ( 1.585 ) ) T(n) = 3T(n/2) +O(n)=>T(n)=O(n^{(1.585)})

d=====( ̄▽ ̄*)b

让我们最后再看一下改进后的整数相乘的伪代码

def Recursive_Multiply(x,y):
    x = x1 * 2**(n/2) + x0
    y = y1 * 2**(n/2) + y0
    # 计算x1+x0 ,y1+y0
    p = Recursive_Multiply(x1+x0,y1+y0)
    
    x1 * y1 = Recursive_Multiply(x1,y1)
    x0 * y0 = Recursive_Multiply(x0,y0)
    return x1 * y1 * 2**n + (p-x1 * y1 - x0 * y0) * 2**(n/2) + x0 * y0

小编是个菜鸡,若有错误请大佬们不吝指正

发布了40 篇原创文章 · 获赞 57 · 访问量 2749

猜你喜欢

转载自blog.csdn.net/weixin_44984664/article/details/105212478
今日推荐