分治法是一种很基础的算法,其实现原理是将大问题分解成多个子问题,通过求解子问题来得到原问题的解,而这些子问题的求解也与原问题一样可以通过将其拆分,求解它们的子问题的解来得到本身的解,这是一种典型的递归思想,因此一般我们编写程序也通常使用递归法来实现分治算法。
下面我们使用分治法求解一道简单的例题来理解分治法的主要思路和解题步骤
给出一个一维数组a[n],使用分治法计算它的所有元素之和,a[n]中的元素如下:
[7, 5, 3, 5, 2, 7, 4]
这是一道很简单的数组求和问题,我们使用蛮力法只需遍历数组所有元素,将其值累加起来就能得到答案。
那么对于分治法该如何求解呢?我们根据上面的分治法的思想,将原问题分解为多个子问题,一般来说我们都会将问题分解为两个子问题,即二分法。
对于本题,我们可以将原问题分解为分别对前半个区间和后半个区间的元素进行求和。
接着,这两个子问题也要同原问题一样进行分解,继续求解它们的子问题。那么这样的分解要到什么时候结束呢,我们知道递归需要有递归体和递归出口,分治法也要有分解的部分和终止条件,而这个终止条件就是问题无法再继续分解下去或者这个问题的解我们已经知道了,此时就没必要再求解下去了,根据实际情况返回结果即可。
对于本题,分治法终止的条件就是待求解数组的长度为1,此时数组中的元素只有一个,该数组的元素之和就是该元素的值。
如下图所示:
掌握了分治法的基本求解步骤后,我们就可以开始设计程序了。
首先给出分治法求解的伪代码:
输入:一维数组a[],求和区间最小下标l,最大下标r
输出:一维数组的和
1. 如果l==r,则求和区间只有一个数据,返回该数据,算法结束
2. 计算划分中点 mid =(l+r)/2;
3. 调用getSum(a,l,mid),得到前半区间的和;
4. 调用getSum(a,mid+1,r),得到后半区间的和;
5. 返回前半区间加后半区间和的和,算法结束;
根据上面的伪代码我们使用递归可以很轻易的写出该算法的代码
public int getSumx(int[] a, int l, int r){
if(l == r)
return a[l];
int mid = (l + r) / 2;
return getSumx(a, l, mid) + getSumx(a, mid + 1, r);
}
通过上面这道题,我们可以得到分治法的基本求解步骤:
首先确定划分条件,根据划分条件将原问题分解为多个小问题,接着对这些小问题分别求解,最后再将小问题的解合并来得到原问题的解。
使用递归我们可以很容易实现分治法,但这并不意味着所有的分治法都需要使用递归,反而,由于递归会消耗更多的时间和空间,一般我们会避免使用递归来实现分治法,而且一些算法也难以使用递归来实现,不过作为初学者,递归实现无疑可以帮助我们更好地理解和实现分治法,我们可以在日后的深入学习中逐渐掌握不递归实现分治法。
最后我们使用分治法再解决一个问题来实践一下
使用二分法求解一维数组的最大值
基本思路:将问题分解为求左半区间的最大值和右半区间的最大值,然后再将左区间的和和右区间的最大值进行比较,返回它们中的较大值。
划分的条件是一维数组的中点,通过数组的划分中点得到两个区间:左区间和右区间
分治的终止条件是数组长度为1,此时数组中只有一个元素,数组的最大值即为该元素。
子问题的合并:将左区间的最大值与右区间的最大值进行比较,得到二者的最大值,此结果即为要求的解。
输入:一维数组a[],求和区间最小下标l,最大下标r
输出:一维数组的最大值
过程:1. 如果l==r,则求和区间只有一个数据,返回该数据,算法结束
2. 计算划分中点 mid =(l+r)/2;
3. 调用getMax(a,l,mid),得到前半区间的最大值;
4. 调用getMax(a,mid+1,r),得到后半区间的最大值;
5. 返回前半区间和后半区间的最大值中较大的值,算法结束;
此问题的分解同上一题,但合并过程不同。
求解步骤如下:
括号中的值为该子问题的解
代码实现:
public int getMax(int[] a, int l, int r){
if(l == r)
return a[l];
return Math.max(getMax(a, l, (l+r)/2), getMax(a, (l+r)/2 + 1, r));
}