一、分治策略简介
在很多情况下,分治策略有可能是简单而强有力的方法,可以用来提高算法的效率
不要问我为什么,他就是这样的……
大概的步骤有三部分
1,将输入的数据分成若干部分
2、递归的求解每个部分的问题
3、将子问题的解组合成为一个全局的解
二、应用领域
分治算法可以被运用到许许多多的方面,可以充当其他算法的一个i组成部分,也可以稍做修改成为另一个算法,例如
归并排序
找平面上最邻近的点对问题
降低整数相乘的复杂度
消除信号的噪音
……
三、算法核心部分分析
马克思和恩格斯教导我们一定要抓住主要矛盾的主要方面
从我的角度看,我们学习分治算法的主要矛盾是将其应用的其他方面
而主要方面就是一定要理解算法的核心部分
这样就可以根据实际情况对分治算法做出适当的修改,提高解决问题的效率
接下来我将会通过解决一下几个问题,让大家比较简单的掌握分治算法
上面以及提到过分治算法的大概过程
我再重述一下
-
将输入的数据分成若干部分
-
递归的求解每个部分的问题
-
将子问题的解组合成为一个全局的解
为了让大家更好的理解这个过程就拿最为熟悉的归并排序算法举例
嘛也不说直接上源代码
python实现
def mergeSort(arr):
if(len(arr)<2):
return arr
middle = math.floor(len(arr)/2)
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)={aT(n/b)+g(n)(n>1)0 (n=1)
这个公式
这个公式就是归并排序的递推公式,下面的很多问题就是围绕这个问题展开的
公式的含义
-
一个规模为n的问题每一次被分成了a个子问题,
-
每个问题的规模是n/b
-
各个子问题的解的复杂度为 g(n)
接下来的这个图你们一定就可以看懂了
这个图就是上面归并排序的分治策略图
n为2的倍数
接下来我们就手动推导出归并排序的复杂度为
T(n)=nlog2n
首先根据
T(n)=aT(bn)+g(n)
我们可以写出
归并排序中每一次被分为2个子问题,每一个子问题的规模是其爸爸的一半,因为归并排序进行排序操作的时候只比较头两个元素的大小,当一半为空时,直接把另一半的其他部分加到结果上就行,所以每一个子问题的复杂度为n
首先从简单的n为2的倍数开始
T(n)=2T(2n)+n
nT(n)=n2T(2n)+1
=2nT(2n)+1
=4nT(4n)+1+1
=(8n)T(8n)+1+1+1
……
=(nn)T(nn)+log2n∗1
=log2n
T(n)=nlog2n
n为自然数
其次对于一般情况我们要证明
T(n)≤n ceiling(log2n)
递推公式为
T(n)<= {0 (n=1)T(ceiling(n/2))+T(floor(n/2))+n (n>1)
我们采用归纳法
当n= 1时显然成立
令
n1=floor(2n) n2=ceiling(2n)
假设
n−1命题时命题成立
T(n)<=T(n1)+T(n2)+n
<=n1(ceiling(log2n1))+n2(ceiling(log2 n2))+n
<=n1(ceiling(log2 n2))+n2(ceiling(log2 n2))+n
=n(ceiling(log2 n2))+n
<=n(ceiling(log2 ceiling(n/2)))+n
<=n(ceiling(log2 n)−1)+n
<=n(ceiling(log2 n))
综上,我们回答刚刚的问题为啥这玩意要在middle处切割的,又为啥要分成两个部分,不分成三个,四个…部分呢?
其实可以在不同地方切割,我们一般选择在middle出切割,也可选择切成多个部分,但是这样的复杂度时不同的
g(n)=c
对于g(n)=c
这个公式最后可以证明
T(n)=O(nlogba)
g(n) = c*n^d
对于
g(n)=cnd
满足如下定理
设函数满足这个递推关系
T(n)=aT(bn)+cnd的增函数,其中
n=bk,a>=1,b是大于1的正整数,c,d为实数且c>=0,d>=0,那么
T(n)=⎩⎨⎧O(nd) a<bdO(ndlog n) a=bdO(nloga b) a>bd
有兴趣的可以自己证明一下,这里就不给出证明了
敲公式实在太累人了ε(┬┬﹏┬┬)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)∗x1+x0
y=2(n/2)∗y1+y0
x∗y=(2(n/2)∗x1+x0)(2(n/2)∗y1+y0)
=2n∗x1∗y1+2(n/2)∗(x1∗y0+x0∗y1)+x0∗y0
我们可以通过递推公式来看一下,现在的算法优劣情况
T(n)=4T(n/2)+O(n)
=>T(n)=O(n2)
O(n) 为每次移位和相加的复杂度
貌似没有啥改变……
接下来看看我们可不可以通过改变a,b,O(n)的值来降低复杂度
可以看出O(n),没有啥可以很好的改进的地方
但是我们可以发现
x∗y=2n∗x1∗y1+2(n/2)∗(x1∗y0+x0∗y1)+x0∗y0
=2n∗x1∗y1+2(n/2)∗((x1+x0)∗(y1+y0)−x1∗y1−x0∗y0)+x0∗y0
这样就成将4次乘法降低为3次了
让我们再来看一次复杂度
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
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
小编是个菜鸡,若有错误请大佬们不吝指正