这两个问题是编程中常见的问题,而且网上有大量博客论述,这里主要是自己做一个笔记。两个之间是有关系的,所以这次放在一起复习
- 最大连续子数组
先看第一个问题:
给定一个整数数组,数组里面可能有正数,负数、零。数组中的一个或多个连续数组构成一个子数组,每个子数组都有一个和,求所有子数组和的最大值。
例子:数组a[]={1,-2,3,10,-4,7,2,5}.那么和最大的子数组为{3,10,-4,7,2},所以结果输出18.
求解:
思路一:暴力法
找出数组的所有子数组,然后求组所有子数组中和最大的,其中确定一个子数组需要起始位置和终止位置,所以这一步的复杂度是
O(n2)
,但是还要讲数组中的元素加起来,所以最后的复杂度是
O(n3)
.
思路二:动态规划方法
假设给你一个数组
A[n]={a1,a2,…,an}
,然后我告诉你前
n−1个数构成的数组的子数组最大和为sn−1,
然后告诉你
an
的大小,你能否告诉我A的最大连续子数组和?答案是不行的,即使最后一个数是正数,我们也不能认为最大和为
Sn−1+an
,因为我们不知道前n-1个数的最大子数组包不包括
an−1
,如果包括那么可以相加,如果不包括无法相加。举个例子
A[]={1,2,3,−10,20}
前四个数的最大子数组是
1,2,3
,所以和为6,虽然20是正数我们不能直接加上去,因为-10不在里面,加上20可能不连续。其实A的最大子数组就是
20
,所以需要一个条件:以
an−1
结束的最大连续子数组的和,假设这个和为
Cn−1
,那么知道
an
,就可以知道:
那么最大子数组的和就是:
还是上面的例子, C4=1+2+3−10=−4,
S4=1+2+3=6,
C5=max{0,−4}+a5=20,
s5=max{C5,S4}=20 ,
所以程序中需要两个变量,一个是以
ai
结尾的最大连续子数组的和currSum(i),一个是去全局最大子数组的和maxSum。初始化均可设置为0,currSum的更新规则为:
函数代码如下:
int maxSubArray(int a[],int n)
{
int maxSum=0;
int currSum=0;
for(int i=0;i<n;i++)
{
if(currSum>=0)
currSum+=a[i];//更新currSum
else
currSum=a[i];
if(currSum>maxSum)
maxSum=currSum;//更新maxSum
}
return maxSum;
}
整个过程中currSum和maxSum更新如下:
拓展
- 如果要求求出最大连续子数组的和,并且同属输出所求子数组的开始位置和起始位置?
分析:这个问题还是比较好解决的,只要清楚什么最大子数组开始的条件和结束的条件就可以了,可以设置几个flag,其中开始点应该是currSum由负数变正数,且currSum大于maxSum的时候。而结束的时候应该是currSum大于maxSum且由正数变为正数。 - 如果要求出最大子数组的和,但不要求子数组是连续的呢?
分析:这个好办,大于0的数相加即可。 - 如果是二维数组,同样要求求出最大连续子数组的和呢?
针对第三个问题,引申出下面第二个问题:
给定一个 M×N 的矩阵,找出此矩阵的一个子矩阵,要求满足这个子矩阵的元素和是最大的,输出这个最大值,如果所有数是负数,就输出0.
例如:给定一个
3×5
的矩阵:
它的和最大子矩阵是:
最后输出和的最大值为17.
当然这里要求的是一个方阵,还有一种更一般的是矩阵中有正有负,对于子矩阵的大小也没有要求:
那么最大子矩阵是:
那么如何求解这个问题呢?
如果采用暴力法——遍历所有子矩阵,那么需要找出所以子矩阵,确定子矩阵需要四个参数,左上角两个,右下角两个所以有
O(n4)
的复杂度,再加上对每个矩阵求和为
O(n2)
的复杂度,所以暴力法的复杂度约为
O(n6)
.
我们在前面已经说到过,最大连续子数组和最大连续子矩阵的和这个问题是有相关性的。当我们确定矩阵的哪几行时,我们如何确定哪几列呢?,例如当我们要选取2,3,4行时,我们需要决定选取那几列的时候,我们只要将这个三行相加,得到一个一维数组,那么就可以用上一个问题的方法去解决了。
所以思路就是:我们遍历所有的行的组成情况,然后将选出来的行按列相加,构成一个一位数组的最大连续子数组问题。
int maxSubMatrix(int a[],int m,int n)
//二维数组以一位数组的形式存放,m和n分别代表矩阵的行和列
{
int max=0;
int sum=-10000; //选择一个足够小的数
int* p=new int[n]; //开辟一个用于存放和的一位数组
for(int i=0;i<m;i++)
{
memset(p,0,sizeof(int)*n); //赋初值为0
for(int j=i;j<m;j++)
{
for(int k=0;k<n;k++)
{
p[k]+=a[j*n+k]; //累加求和
}
max=maxSubArray(p,n); //调用一维数组的方法
if(max>sum)
sum=max;
}
}
delete [] p;
return sum;
}
测试:
int main()
{
int s[]={0,-2,-7,1,9,2,-6,2,-4,1,-4,1,-1,8,0,-2};
cout<<maxSubMatrix(s,4,4)<<endl;
return 0;
}
结果是:15
延伸问题:长度最短连续子序列问题
有一个长度为 n <script type="math/tex" id="MathJax-Element-42">n</script>的正整数序列,现给定一个整数S,要求求出序列中长度最短的一个连续序列,且序列的和大于等于S。
分析:可以直接用两个for循环枚举所有子序列的起点和终点,但这种方法的时间复杂度为$O(n^3)$,需要找到更好的办法。
其实,因为都是正数,所以当从第一个数开始累加,累加到大于S的时候,开始删掉前面的数,直到小于S,然后把后面的数加进来,这样遍历一遍就能解决掉。
int LessSeq(const int a[],int N,int s)
{
int start,end,sum;
start=end=sum=0;
int L=N+1;
while(end<N)
{
if(sum<s)
sum+=a[end];
while(sum>=s)
{
sum-=a[start];
L=min(L,end-start+1);
start++;
}
end++;
}
return L;
}