最大和子数组问题
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
13 | -3 | -25 | 20 | -3 | -16 | -23 | 18 | 20 | -7 | 12 | -5 | -22 | 15 | -4 | 7 |
求这个数组中子数组的最大和。
分治法的思想:
我们来思考如何用分治法来求解最大子数组问题。假定我们要寻找子数组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[low+1,...,high]中或者跨越中点的所有子数组中和最大者。我们可以递归求解A[low,...,high]和A[mid+1,..,high]的最大子数组,因为这两个子问题仍然是最大子数组问题,只是规模更小。因此,剩下的全部工作就是寻找跨越中点的最大子数组,然后再三种情况中选择最大者。
寻找跨越中点的最大子数组(O(N))伪代码:
find_max_crossing_subarray(a,low,mid,hight)
left_sum = -65535
sum = 0
for i = mid dwonto low
sum = sum + A[i]
if sum > left_sum
left_sum = sum
max_left = i
right_sum = -65535
sum = 0
for j=mid+1 to high
sum = sum +A[j]
if sum>right_sum
right_sum = sum
max_right = j
return(max_left,max_right,left_sum+right_sum)
有了一个线性时间的find_max_crossing_subarray,我们就可以设计求解最大子数组问题的分治算法的伪代码了:
find_max_sumarray(A,low,high)
if high==low
return(low,high,A[low]) //base case:only one element
else mid = (low+high)/2
(left_low,left_high,left_sum)= find_max_sumarray(A,low,mid)
(cross_low,cross_high,right_sum) = find_max_sumarray(A,mid+1,high)
if left_sum >= right_sum and left_sum >= cross_sum
return (left_low,left_high,left_sum)
elseif right_sum >= left_sum and right_sum >= cross_sum
return (right_low,right_hight,right_sum)
else return (cross_low,cross_high,cross_sum)
分治算法的时间分析:
T(n)={Θ(1)2T(n/2)+Θ(n)ifn=1ifn>1
求解为T(n)=Θ(nlgn)。
我们利用分治法得到了一个渐进复杂性优于暴力求解方法的算法。通过归并排序和本次的最大子数组问题,我们开始对分支方法的强大能有了一些了解。有时,对某个问题,分支方法能给出最快的算法,而其他时候,我们(不用分治方法)甚至能做得更好。以此为例,不用分治法,而用动态规划的思想求解本题能得到线性时间复杂度的算法。
非递归的思考
使用如下思想为最大子数组问题设计一个非递归的,线性时间的算法。从数组的左边界开始,由左至右处理,记录到目前为止已经处理过的最大子数组。若已知A[1,...,j]的最大子数组,基于如下性质将解扩展为A[1,..,j+1]的最大子数组:A[1,..,j+1]的最大子数组要么是A[1,..,j]的最大子数组,要么是某个子数组A[i,...,j+1](1<=i<=j+1)。在已知A[1,..,j]的最大子数组的情况下,可以在线性时间内找出形如A[i,...j+1]的最大子数组。
怎么找出来呢?
- 一个O(n)的算法:
设F[j]为A[1..j]数组的最大和子数组。
我只想到F[j]=max1≤i≤j{F[j?1],∑A[i,..,j]}
如果直接这样编程,时间就不是线性的了。如果我们稍作修改呢?
设Sum[0,0]=0;∵∑A[i,...,j]=Sum[0,...,j]?Sum[0,...,i]?F[j]=max{F[j?1],Sum[0,..,j]?min0≤i≤jSum[0,..i]}
我们可以先计算出min0≤i≤jSum(0,..i)来,
设sum(i)=Sum[0,...i],sum(0)=0
则:sum(i)=sum(i?1)+A[i],1≤i≤i,
可以得到以下步骤:- 求前缀数组和以及最小和前缀
- 定义:前缀和sum[i] = A[1]+...+A[i],sum[0]=0。
并且定义: m[i]是A[1,...,i]子数组中最小和的前缀串。m[0] = 0. - 则:sum[j]=sum[j?1]+A[j].
顺带求出m[j]=min(m[j?1],sum[j])
- 定义:前缀和sum[i] = A[1]+...+A[i],sum[0]=0。
- F[j]=max{F[j?1],sum[j]?m[j]}
因为每一步都是线性的,所以总的时间复杂是O(n)。
可以看到其实sum[j] - m[j]就是以A[j]结尾的子数组最大和。如果我们把这部分单独拿出来考虑,可以得到另一种方法:
- 求前缀数组和以及最小和前缀
- 另一个稍作改进的算法
既然sum[j]-m[j]是以A[j]结尾的最大子数组和S[j],那么我们求出所有的S[1],...,S[n],然后求一个最大的,不就得到解了么?- 求前缀数组和以及最小和前缀
- 定义:前缀和sum[i] = A[1]+...+A[i],sum[0]=0。
并且定义: m[i]是A[1,...,i]子数组中最小和的前缀串。m[0] = 0. - 则:sum[j]=sum[j?1]+A[j].
顺带求出m[j]=min(m[j?1],sum[j])
那么,S[j] = sum[j] - m[j]
- 定义:前缀和sum[i] = A[1]+...+A[i],sum[0]=0。
- 求出S[i,...,N]的最大值
- 求前缀数组和以及最小和前缀
利用动态规划来解决
最优子结构:
设S[j]是以元素A[j]结尾的和最大子数组。那么已知j以前的状态,S[0],..,S[j-1]怎么求S[j]呢?
其实
S[j]=max{S[j?1]+A[j],A[j]}
重叠子问题:
如果直接这样递归的去做,肯定会重复计算很多次相同的子问题。
所以我们改成迭代,经过O(n)时间复杂度就可以求出答案。
*代码如下:
/*求最大子数组的和,以及返回这个数组本身*/
int MaxSubarray(const int * a, int size, int & from, int & to){
if (!a || (size <= 0)){
from = to = -1;
return 0;
}
from = to = 0;
int sum = a[0];
int result = sum;
int fromNew = 0; // 新的子数组起点
for (int i = 0; i < size; i++){
if (sum>0){
sum += a[i];
}
else{
sum = a[i];
fromNew = i;
}
if (result < sum){
result = sum;
from = fromNew;
to = i;
}
}
return result;
}