问题:
一个有N个整数元素的一维数组(A[0]、A[1],...A[n-1]),求子数组之和的最大值。
注意:1.题目中说的子数组,是连续的;
2.题目只求和,不需要返回子数组的具体位置;
3.数组的元素是整数,所以数组可能包含正整数、零和负整数。
解答:
方法一:暴力枚举法
枚举所有子数组之和,设sum[i,···,j]为数组中第i个元素到第j个元素的和(其中0<=i<=j<=n),遍历所有可能的sum[i,···,j],那么时间复杂度为O(N^3)。
//方法一:暴力枚举
//对于每一个给定的元素i,j有N种不同的取值,采取双指针指定范围后用k指针扫描i到j的范围
int MaxSum(int arr[] , int arrLen )
{
int sum;
int maxSum = -9999;
for(int i=0 ; i<arrLen ; i++){
for(int j=i ; j<arrLen ; j++){
sum = 0;//变动一次j指针,sum清零
for(int k=i ; k<=j ; k++){
sum += arr[k];
}
if(sum > maxSum)
maxSum = sum;
}
}
return maxSum;
}
如果注意到连续性,也就是sum[i,······j] = sum[i,······,j-1]+arr[j],可以在O(N^2)求得结果,不需要扫描指针k。
//方法二:暴力枚举二
//对于每一个给定的元素i,j有N种不同的取值,采取双指针,直接累加比较
int MaxSum(int arr[] , int arrLen)
{
int maxSum = -9999;
int sum;
for(int i=0 ; i<arrLen ; i++){
sum = 0;
for(int j=i ; j<arrLen ; j++){
sum += arr[j];
if(sum > maxSum)
maxSum = sum;
}
}
return maxSum;
}
将数组划分两部分,分别求出这两部分的最大和MaxL与MaxR,则最大和在MaxL与MaxR或是横跨中间元素A[i]的子数组中。若最大子数组和中包括A[i]这个元素,则从A[i]往左找,找出左边的最大值,再从A[i]往右找,找出右边的最大值,相加即得。本方法规模减半,加上一次遍历数组,故T(N) = O(NlogN)
代码如下:
//方法三:分治法
//求出MaxL与MaxR,再从中间元素向前追溯最大子数组,以及从中间向后追溯最大子数组,二者相加与MaxL、MaxR取最大
int MaxSum(int arr[] , int low , int high)
{
//递归截止条件
if(high-low <=0)
return arr[high];
int mid = low + (high-low)/2;//防止上溢
int MaxL = MaxSum(arr , low , mid);
int MaxR = MaxSum(arr , mid+1 , high);
//从中心元素向前扫描
int sum = 0;
int maxLSum = -9999;
for(int i=mid ; i>=0 ; i--){
sum += arr[i];
if(sum > maxLSum)
maxLSum = sum;
}
//从中心元素向后扫描
sum = 0;
int maxRSum = -9999;
for(int i=mid+1 ; i<=high ; i++){
sum += arr[i];
if(sum > maxRSum)
maxRSum = sum;
}
sum = maxRSum+maxLSum;
//返回MaxL、MaxR、maxLsum+maxRsum最大
int max;
max = MaxL>MaxR?MaxL:MaxR;
max = max>sum?max:sum;
return max;
}
方法三:动态规划法
解法二中的方法已经将O(N^2)复杂度降低至O(NlogN),能否进一步降低复杂度呢?考虑N个元素的数组转化为较小的问题(N-1个元素),自底向上的考虑,假设知道(A[1]、A[2]、······A[n-1])中最大的一段数组和为All[1],以A[1]为首的子数组最大和为Start[1],则如果将A[0]加入数组中,此时造成的影响是:
A[0]本身为整个数组的最大数组和,或以A[0]为首连接A[1]的那段最大子数组和,或A[0]的加入没有影响,因此
All[0] = max(A[0] , A[0]+Start[1] , All[1]);可以看出这个问题符合无后效性,可以使用动态规划的方法解决。
//方法四:动态规划法
//根据n-1个元素的数组情形推定n个元素的数组情形,有all[0] = max(all[1] , start[1]+A[0] , A[0])
int MaxSum(int arr[] , int arrLen)
{
//-----初始化底部条件
int All,Start;
All = arr[arrLen-1];
Start = arr[arrLen-1];
//-----自底向上求解
for(int i=arrLen-2 ; i>=0 ; i--){
if(Start+arr[i] < arr[i])//或说Start<0
Start=0;
Start = Start+arr[i];
All = All>Start?All:Start;
}
return All;
}
扩展问题1:如果数组允许首尾相邻,也就是允许找到数字(A[i],···,A[n-1],A[0],···,A[j]),使它的和最大,怎么办?
解答:可以这样分为两种情况:一种是穿过首尾的,一种是不穿过首尾的(原问题)。对于穿过首尾元素的,必然会是(A[0]···A[n-1])或(A[i],···,A[n-1],A[0],···,A[j]),也就是整个数组或是从整个数组中扣去子数组和为负数且绝对值最大的那一组。这样取二情况最大的就是了。总时间复杂度O(N)+O(N)=O(N)。
代码如下:
//扩展问题一:动态规划法
//考虑越过尾部到首部的情形,需要多扫描一次考虑求解从头到尾数组元素和为负,且绝对值最大的那一串和。
int MaxSum(int arr[],int arrLen)
{
int All,Start,maxS;
//---初始化,自底向上求解不考虑首尾相邻的情况
All = arr[arrLen-1];
Start = arr[arrLen-1];
for(int i=arrLen-2 ; i>=0 ; i--){
if(Start < 0)
Start = 0;
Start += arr[i];
if(All < Start)
All = Start;
}
//---求解数组中绝对值最大且和为负数的一段子数组元素和,相当于求最小子数组和然后判定是否子数组和为负
int minAll,minStart;
minAll = arr[arrLen-1];
minStart = arr[arrLen-1];
for(int i=arrLen-2 ; i>=0 ; i--){
if(minStart > 0)
minStart = 0;
minStart += arr[i];
if(minAll > minStart)
minAll = minStart;
}
//---求整个数组和,并比较三种情况哪种最大
int sum = 0;
for(int j=0 ; j<arrLen ; j++)
sum += arr[j];
if(minAll < 0)
maxS = sum-minAll;
else
maxS = sum;
if(maxS < All)
maxS = All;
return maxS;
}
扩展问题二:如果要求同时返回最大子数组的位置,算法如何改变?还能保持O (N)的时间复杂度吗?
//方法四:动态规划法
//根据n-1个元素的数组情形推定n个元素的数组情形,有all[0] = max(all[1] , start[1]+A[0] , A[0])
//使用两对指针分别更新Start与All变量变化时指针的变化
int MaxSum(int arr[] , int arrLen ,int &begin , int &end)
{
//-----初始化底部条件
int All,Start;
All = arr[arrLen-1];
Start = arr[arrLen-1];
begin = end = arrLen-1;
int Sbegin = arrLen-1;
int Send = arrLen-1;
//-----自底向上求解
for(int i=arrLen-2 ; i>=0 ; i--){
if(Start+arr[i] < arr[i])//或说Start<0
{
begin = i;
end = i;
Start=0;
}else{//将A[i]并入Start中
begin = i;
}
Start = Start+arr[i];
if(All < Start){
Sbegin = begin;
Send = end;
All = Start;
}
}
begin = Sbegin;
end = Send;
return All;
}
可以设定两对指针,当Start<0更新start的参考指针begin与end指向,考虑5 -4 1 2,对于此刻-4 1 2,all=3,start=-1,下一次迭代考虑5时,由于start<0,可以更新start参考指针begin与end指向5处,此时以5为首的最大子数组和为5。
all的参考指针Sbegin与Send依旧指向原位置1与2处,这样当此时start计算后与all进行比较,此处比较5与3,可以看到考虑了5后all应该变大了,于是更新all的参考指针Sbegin与Send为上一次Start的参考指针begin与end。