分治法是构建基于多项分支递归的一种很重要的算法范式。字面上的解释是「分而治之」,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
分治法的适用场景:对于一个规模为 n 的问题,若该问题可以容易地解决(比如说规模 n 较小)则直接解决,否则将其分解为 k 个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。
下面我通过一个简单的实例题目对分治进行详解:
**最大连续和问题:**给出一个长度为n的序列A1,A2,···,An
,求最大连续和。换句话说,要求找到 1<=i<=j<=n
,使得Ai+···+Aj
尽量大。
分治算法解题一般分为三个步骤:
划分问题: 把问题的实例划分成子问题;
递归求解: 递归解决子问题;
合并问题: 合并子问题的解得到原问题的解。
“划分”就是把序列分成元素个数尽量相等的两半;
“递归求解”就是分别求出完全位于左半边或则完全位于右半边的最佳序列;
“合并”就是求出起点位于左半、终点位于右半的最大连续和序列,并和子问题的最优解比较。
“划分”和“递归求解”基本上都能理解,关键在于“合并” 步骤。既然起点位于左半,终点位于后半,则可以认为把这样的序列分成两个部分,然后独立求解:先寻找最佳起点,然后再寻找最佳终点。
**最大序列和问题:**给出一个长度为 n 的序列$$A_1$$,$$A_2$$,···,$$A_n$$
,求最大连续和。换句话说,要求找到 1<=i<=j<=n
,使得$$A_i$$+$$A_{i+1}$$+···+$$A_j$$
尽量大。
假如现在有一组数[2,4,-2,1]
,那么这组数的连续和有以下几种情况:
2=2
4=4
-2=-2
1=1
2+4=6
4-2=2
-2+1=-1
2+4-2=4
4-2+1=3
2+4-2+1=5
不难看出,组数[2,4,-2,1]
的最大序列和为 6。这里采用的是穷举法,列举出了所有可能的结果,当数组长度比较大时,运算复杂度会非常大。
很容易看出,最大序列和问题适用于分治法。现在介绍一下如何用分治法来求解最大序列和问题,根据分治法的解题步骤,对应的解题思路如下:
“划分”就是把序列分成元素个数尽量相等的左右两半;
“递归求解”就是分别求出完全位于左半边或者完全位于右半边的最佳序列;
“合并”就是求出起点位于左半、终点位于右半的最大连续和序列,并和子问题的最优解比较。
“划分”和“递归求解”基本上都能理解,关键在于“合并” 步骤。既然起点位于左半,终点位于右半,则可以认为把这样的序列分成两个部分,然后独立求解:先寻找最佳起点,然后再寻找最佳终点。
这里要求一组数的最大序列和,我们可以把数组分成两半,求另外两半数据的最大序列和,如果还是不能够解决问题,我们就再分两半,直到我们很容易做出判断为止,此时我们的“划分”,“递归求解”结束。例如:[2,1,-1,5,-1]
,第一次“划分”后分成了两小块[2,1]
和 [-1,5,-1]
;很容易看出,[2,1]
小块的最大序列和是为3
,[-1,5,-1]
小块的最大序列和是3
。那么[2,1,-1,5,-1]
的最大序列和是多少呢?我们肉眼可见就是7
。下面讲述一下“合并的过程”!
首先,我们需要明确一点,最大序列和有三种情况:
1.分两半之后,左边的最大序列和(简写“Lmax”)直接是整个序列的最大连续和;
2.分两半之后,右边的最大序列和(简写“Rmax”)直接是整个序列的最大连续和;
3.分两半之后,整个序列的最大连续和是整个序列的中间部位(简写“L+R”),即最大连续和包含左边的一部分数据和右边的一部分数据。
由上可得,[2,1,-1,5,-1]
的最大连续序列和 = MAX(Lmax,Rmax,L+R)。
其次,我们要确定每次“划分”的分界点,这里用 m 表示,计算公式如下所示:
m=(序列长度-序列第一个值的索引)/2 # m 需要取整
例如:数组[2,1,-1,5,-1]
第一次“划分”时,序列长度是 5,第一个序列值是 2,它的索引是 0。由公式可得,m=(5-0)/2=2
(需要取整)。
在“划分”时,遵循“左开右闭”的规则。以数组[2,1,-1,5,-1]
为例,第一次“划分”的 m 值为 2,根据“左开右闭”的规则,可以划分为两小块[2,1]
和 [-1,5,-1]
,即索引值为 2 的值被右小块取到了。
然后,需要依次求取 Lmax、Rmax、L 和 R 的值:
左小块[2,1]
的 Lmax 值为 3;
右小块[-1,5,-1]
的 Rmax 值为 5;
L
=以索引m-1
为起点的左边序列和的最大值,索引每次向左移一步,再累加求和,对于左小块[2,1]
,L=max(1,1+2)=3
。
R
=以索引m
为起点的右边序列和的最大值,索引每次向右移一步,再累加求和,对于右小块[-1,5,-1]
,R=max(-1,-1+5,-1+5-1)=4
。
最后,进行合并求解,best=max(Lmax,Rmax,L+R)=max(3,5,3+4)=7
。这里没有进行递归演示,其实在求解右小块[-1,5,-1]
的 Rmax 值时,用到了公式best=max(Lmax,Rmax,L+R)
。
经上面的演算,分治法的 C++ 代码直接推导为:
int maxsum(int* A,int x,int y){ // 返回数组在左闭右开区间[x,y)中最大连续和
int v,L,R,maxs;
if(y-x==1) return A[x]; // 只有一个元素,直接返回
int m=x+(y-x)/2; // 分治第一步:划分成[x,m)和[m,y)
maxs=max(maxsum(A,x,m),maxsum(A,m,y)); // 分治第二步:递归求解
v=0;L=A[m-1]; // 分治第三步:合并(1)——从分界点开始往左的最大连续和L
for(int i=m-1;i>=x;i--) L=max(L,v+=A[i]);
v=0; //分治第三步:合并(2)——从分界点开始往右的最大连续和R
R=A[m];
for(int i=m;i
数据结构、算法嘛!讲究效率。虽然对于最大序列和问题,用分治法相比于传统的方法看上去很巧妙,时间复杂度上也还过得去,分治法的时间复杂度是 O(n logn)
。但是对于这道题,分治法还不是最高效的,算法还可以再优化,时间复杂度可以达到 O(n)
。由于本章讲的是分治法
,这里我就不做过多的阐述了。
一个在下游疯狂打怪的少年。希望早点遇见大魔王。