1. 分治:分而治之,即将较大规模的问题分解成几个较小规模的问题,通过对较小规模问题的求解达到对整个问题的求解。
当我们将问题分解成两个较小问题求解时的分治方法称之为二分法。
2. 基本思想(基本策略)
(1)分——分解(Divide):将一个规模为n的问题划分为k个规模更小的子问题;
(2)治——解决(Conquer):递归地求解子问题。若子问题的规模足够小则停止递归,直接求解;
(3)合——合并(Combine):将子问题的解组合成原问题的解。
其中:当子问题足够大,需要递归求解时,我们称之为“递归情况”(Recursive Case);当子问题变得足够小,不再需要递归时,我们说递归已经“触底”,进入“基本情况”(Base Case)。
二、典例
1. 最大子数组问题
输入n及一个整型数组(n个数)。数组中连续的一个或多个元素组成一个子数组,每个子数组都有一个和。求所有子数组的和的最大值。(1<=n<=100000, n个数可正可负也可为0)
样例输入1:
8
1 -2 3 10 -4 7 2 -5
样例输出1:
18
3 10 -4 7 2
样例输入2:
16
13 -3 -25 20 -3 -16 -23 18 20 -7 12 -5 -22 15 -4 7
样例输出2:
43
18 20 -7 12
【分析】经典的“子序列”问题,目标是在一个整型数组中找到一个和最大的子数组。
注意以下两点:
①我们通常说“一个最大子数组”,而不是“最大子数组”,因为可能有多个子数组达到最大和;并且只有当数组中包含负数时,最大子数组问题才有意义:换个角度想,如果所有数组元素都是非负的,最大子数组问题没有任何难度,因为整个数组的和肯定是最大的。
②有以下三种解法可入手:暴力枚举/分治/动态规划
法一:暴力枚举,复杂度O(n2)(当数据规模较大时应另谋他法!)
记sum[i..j]为数组中第i个元素到第j个元素的和(其中0<=i<=j<=n-1),通过遍历所有的组合之和,即可找到最大的一个和(即所求出的子数组的起点和终点)
#include <stdio.h>
#include <math.h>
#define maxn 100005
#define MIN -(pow(2,31)-1)
int n;
int a[maxn];
int maxsum; //所求得最大子数组的和
int maxleft,maxright; //所求得最大子数组的起点与终点
int findMax(int *a) //求最大子数组的和
{
int i,j;
int left,right; //left.right分别记录子数组起点与终点
int cursum,result=MIN; //cursum-当前子数组的和 result-所求解(注意初始化)
//枚举子数组起点
for(i=0;i<n;i++)
{
cursum=0;
left=i;
//枚举子数组终点(1<=子数组长度<=n)
for(j=i;j<n;j++)
{
cursum+=a[j];
//若当前子数组的和>最大子数组之和,则更新结果及下标
if(cursum>result)
{
right=j;
maxleft=left;
maxright=right;
result=cursum;
}
}
}
return result;
}
int main()
{
int i;
scanf("%d",&n);
for(i=0;i<n;i++)
scanf("%d",&a[i]);
maxsum=findMax(a);
//输出最大子数组的和
printf("%d\n",maxsum);
//输出最大子数组中的各元素
for(i=maxleft;i<maxright;i++)
printf("%d ",a[i]);
printf("%d\n",a[maxright]);
return 0;
}
法二:分治(核心),复杂度O(nlogn)
假定我们要寻找子数组A[low...high]的最大子数组,使用分治技术意味着我们要将子数组划分为两个规模尽量相等的子数组,也就是说找到子数组的中央位置mid,然后考虑求解两个子数组A[low...mid]和A[mid+1...high]。
A[low...high]的任何连续子数组A[i...j]所处的位置必然是以下三种情况之一:
①完全位于子数组A[low...mid]中,因此low<=i<=j<=mid;
②完全位于子数组A[mid+1, high]中,因此mid<i<=j<=high;
③跨越中点,因此low<=i<=mid<j<=high。
从而,我们最终求得的解必然是以上3种情况中的子数组和的最大者;
且对于A[low...mid]和A[mid+1...high]的最大子数组,我们可以递归求解(因为这两个问题是规模更小的最大子数组问题),剩下的工作便是寻找跨越中点的最大子数组,然后在三种情况中选取和最大者。
故求解过程如下:
(1)分——将原数组拆分成两部分A[low...mid]和A[mid+1...high],每个部分再拆分成新的两部分……直到数组被分得只剩下一个元素;
(2)治——在划分后的小数组中找最大子数组。对于划分后只有一个元素的数组,解就是该元素;
(3)合——将两个小数组合并为一个数组,其中解有三种可能:
左边的返回值大,右边的返回值大,中间存在一个更大的子数组和。返回值应选最大的。
(4)此外,特别注意:以上3种情况的难点在于两个数组合并的时候,位于两个数组中间位置存在最大和的情况,处理方法为:
①从中间位置开始,分别向左和向右两个方向进行操作,通过累加找到两个方向的最大和,分别为lsum和rsum,因此存在于中间的最大和为(lsum+rsum);
②向左的累加操作和向右的累加操作完全一样,只需要一层循环就可以解决问题:
1°初始化lsum、rsum为最小值,cursum=0用于累加和;
2°在向左累加的操作中, cur