分治策略
回忆一下,在归并排序中,我们谈到了分治策略。那什么是分治策略?
在分治策略中,我们递归地求解一个问题,在每层递归中应用如下三个步骤:
- 分解(Divide) 步骤将问题分为一些子问题,子问题的形式与原问题一样,只是规模更小。
- 解决(Conquer) 步骤递归地求解出子问题。如果子问题的规模足够小,则停止递归,直接求解。
- 合并(Combine) 步骤将子问题的解组合成原问题的解。
当子问题足够大,需要递归求解时,我们称之为递归情况。当子问题变得足够小,不再需要递归时,我们说递归已经“触底”,进入了基本情况。有时,除了与原问题形式完全一样的规模更小的子问题外,还需要求解与原问题不完全一样的子问题。我们将这些子问题的求解看做合并步骤的一部分。
好了,进入正题!
最大子数组问题
假定你入职了一家上市公司,这家公司发行自己的股票,但是这家股价比较不稳定。接着你被准许可以在某个时刻买进一股该公司的股票,并在之后某个日期将其卖出,买进卖出都是在当天交易结束后进行。为了补偿这一限制,你可以了解股票将来的价格。你的目标是最大化收益。我们给出了17天内的股票价格。第0天的股票价格是每股100美元,你可以在此之后任何时间买进股票。你当然希望“低价买进,高价卖出”—在最低价格时买进股票,之后在最高价格时卖出,这样可以最大化收益。但遗憾的是,在一段给定的时期内,可能无法做到在最低价时买进股票,然后在最高价时卖出。例如,如下图,最低价格发生在第7天,而最高价格发生在第1天—最高价在前,最低价在后。
横轴表示日期,纵轴表示股票价格。表格的最后一行给出了股票价格相对于前一天的变化
问题变换
我们从一个稍微不同的角度来看待输入数据。我们的目标是寻找一段日期,使得从第一天到最后一天的股票价格净变值最大。因此,我们不再从每日价格的角度去看待输入数据,而是考察每日价格变化,第i天的价格变化定义为第i天和地i-1天的价格差。上图表格的最后一行给出了每日价格变化。如果将这一行看做一个数组A,如下图所示,那么问题就转化为寻找A的和最大的非空连续子数组。我们称这样的连续子数组为最大子数组。例如,对下图中的数组,A[1…16]的最大子数组为A[8…11],其和为43。因此,你可以在第8天(7天之后)买入股票,并在第11后卖出,获得每股收益43$
接下来,我们寻找最大子数组问题的更高效的求解方法。在此过程中,我们通常说“一个最大子数组”而不是“最大子数组”,因为可能有多个子数组达到最大和。
只有当数组中包含负数时,最大子数组问题才有意义。如果所有数组元素都是非负的,最大子数组问题没有任何难度,因为整个数组的和肯定是最大的。
使用分治策略的求解方法
我们来思考如何利用分治技术来求解最大子数组问题。假定我们要寻找子数组 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。
因此,A[low…high] 的一个最大子数组所处的位置必然是这三种情况之一。实际上,A[low…high] 的一个最大子数组必然是完全位于A[low…mid] 中、完全位于A[mid+1…high] 或者跨越中点的所有子数组中和最大者。我们可以递归地求解A[low…mid] 和A[mid+1…high] 的最大子数组,因为这两个子问题仍是最大子数组问题,只是规模更小。因此,剩下的全部工作就是寻找跨越中点的最大子数组,然后在三种情况中选取和最大者。
我们可以很容易地在线性时间内求出跨越中点的最大子数组。此问题并非原问题规模更小的实例,因为它加入了限制- - -求出的子数组必须跨越中点。任何跨越中点的数组的子数组都由两个子数组A[i…mid] 和A[mid+1…j] 组成,其中low≤i≤mid且mid<j≤high。因此,我们只需找出形如A[i…mid] 和A[mid+1…j] 的最大子数组,然后将其合并即可。过程FIND_MAX_CORSSING_SUBARRAY接收数组A和下标low、mid和high为输入,返回一个下标数组划定跨越中点的最大子数组的边界,并返回最大子数组中值的和。
FIND_MAX_CORSSING_SUBARRAY
1 left_sum=-∞
2 sum=0
3 for i=mid downto low
4 sum=sum+A[i]
5 if sum>left_sum
6 left_sum=sum
7 max_left=i
8 right_sum=-∞
9 sum=0
10 for j=mid+1 to high
11 sum=sum+A[j]
12 if sum>right_sum
13 right_sum=sum
14 max_right=j
15 return (max_left,max_right,left_sum+right_sum)
如果子数组A[low…high]包含n个元素(即n=high-low+1),则调用FIND_MAX_CORSSING_SUBARRAY(A,low,mid,high)花费Θ(n)时间。由于两个for循环的每次迭代花费Θ(1)时间,我们只需统计一共执行了多少次的迭代。第37行的**for**循环执行了mid-low+1次迭代,第1014行的for循环执行了high-mid次迭代,因此总循环迭代次数为:
(mid-low+1)+(high-mid)=high-low+1=n
有了一个线性时间的FIND_MAX_CROSSING_SUBARRAY在手,我们就可以设计求解最大子数组问题的分治算法的伪代码了:
FIND_MAXIMUM_SUBARRAY(A,low,high)
1 if high==low
2 return(low,high,A[low])
3 else mid=(low+high)/2 (向下取整)
4 (left_low,left_high,left_sum)=FIND_MAXIMUM_SUBARRAY(A,low,mid)
5 (right_low,right_high,right_sum)=FIND_MAXIMUM_SUBARRAY(A,mid+1,high)
6 (cross_low,cross_high,cross_sum)=FIND_MAX_CROSSING_SUBARRAY(A,low,mid,high)
7 if left_sum ≥ right_sum and left_sum ≥ cross_sum
8 return (left_low,left_high,left_sum)
9 else if right_sum ≥ left_sum and right_sum ≥ cross_sum
10 return (right_low,right_high,right_sum)
11 else return (cross_low,cross_high,cross_sum)
初始调用FIND_MAXIMUM_SUBARRAY(A,1,A.length)会求出A[1…n]的最大子数组
与FIND_MAX_CROSSING_SUBARRAY相似,递归过程FIND_MAXIMUM_SUBARRAY返回一个下标元祖,划定了最大子数组的边界,同时返回最大子数组中的值之和。第1行测试基本情况,即子数组只有一个元素的情况。在此情况下,子数组只有一个子数组- - -它自身,因此第2行返回一个下标元祖,开始和结束下标均指向唯一的元素,并返回此元素的值作为最大和。第3~11行处理递归情况。第3行划分子数组,计算中点mid。我们称子数组A[low…mid]为左子数组,A[mid+1…high]为右子数组。因此我们知道子数组A[low…high]至少包括两个元素,则左、右两个子数组各至少包含一个元素。第四行和第五行分别递归地求解左右子数组中的最大子数组。第6~11行完成合并工作。第6行求跨越中点的最大子数组(回忆一下,第6行求解的子问题并非原问题的规模更小的实例,我们将其看做合并部分)。第7行检测最大和子数组是否在左子数组中,若是,第8行返回此子数组。否则,第9行检测最大和子数组是否在右子数组中,若是,第10行返回此子数组。如果左、右数组均不包含最大子数组,则最大子数组必然跨越中点,第11行将其返回。
java代码
class Find_Maximum_Subarray{
public static void main(String[] args){
int[] a = {13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7};
int low=0;
int high=a.length-1;
int[] result = find_maximum_subarray(a,low,high);
System.out.println("数组起始坐标:"+result[0]+"\n终点坐标:"+result[1]+"\n最大子数组的和:"+result[2]+"美元");
}
public static int[] find_maximum_subarray(int[] a,int low,int high){
if(low==high){
int[] result = {low,high,a[low]};
return result;
}else{
int[] result = new int[4];
int mid = (low+high)/2;
int[] left_result = new int[3];
left_result = find_maximum_subarray(a, low, mid).clone();
int[] right_result = new int[3];
right_result = find_maximum_subarray(a, mid+1, high).clone();
int[] cross_result = new int[3];
cross_result = find_max_crossing_subarray(a,low,mid,high).clone();
if(left_result[2]>=right_result[2]&&left_result[2]>=cross_result[2]){
result[0]=left_result[0];
result[1]=left_result[1];
result[2]=left_result[2];
return result;
}else if(right_result[2]>=left_result[2]&&right_result[2]>=cross_result[2]){
result[0]=right_result[0];
result[1]=right_result[1];
result[2]=right_result[2];
return result;
}else{
result[0]=cross_result[0];
result[1]=cross_result[1];
result[2]=cross_result[2];
return result;
}
}
}
public static int[] find_max_crossing_subarray(int[] a,int low,int mid,int high){
mid=a.length/2;
int left_sum=-(Integer.MAX_VALUE);
int sum=0;
int max_left = mid;
for(int i=mid-1;i>=0;i--){
sum+=a[i];
if(sum>left_sum){
left_sum = sum;
max_left = i;
}
}
int right_sum = -(Integer.MIN_VALUE);
sum = 0;
int max_right = mid;
for(int i=mid;i<a.length;i++){
sum+=a[i];
if(sum>right_sum){
right_sum = sum;
max_right = i;
}
}
int result[] = {max_left,max_right,left_sum+right_sum};
return result;
}
}
测试结果: