算法:[c++]求一数组最大字段和(最大子数组)的几种方法)
给定由 n n n个整数(可能为负数)组成的序列 a 1 , a 2 , a 3 , . . . , a n a_1,a_2,a_3,...,a_n a1,a2,a3,...,an,求该序列形如 ∑ k = i i a k \sum_{k=i}^ia_k ∑k=iiak的字段和的最大值。
1.简单算法:
最直接的方法,穷举所有可能的字段,计算每段的和,找出最大的一段。
设每个段的起始位置(数组下标)为 i i i,终止位置为 j j j,求 a i + . . . + a j a_i+...+a_j ai+...+aj。
int n=11;
int a[n] = {-1,2,4,-7,6,2,-3,5,-9,7,3};
int sum,max=-9999; //max为负无穷
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
sum=0;
for(int k=i;k<=j;k++){
sum += a[k];
}
if(sum>max){
max=sum;
}
}
}
cout<<"最大字段和为"<<max;
上述算法共三层循环,不难看出时间复杂度为T(n)=O(n^3)。
注意到,在计算 a i + . . . + a j + 1 a_i+...+a_{j+1} ai+...+aj+1时,可利用已经计算过的 a i + . . . + a j a_i+...+a_j ai+...+aj的值,因此可以改进此算法。
int n=11;
int a[n] = {-1,2,4,-7,6,2,-3,5,-9,7,3};
int sum,max=-9999; //max为负无穷
for(int i=0;i<n;i++){ //每循环一次,计算了所有以a[i]为起始元素的字段的值
sum = 0;
for(int j=i;j<n;j++){ //a[j]为当前字段结束元素
sum+=a[j];
if(sum>max){
max=sum;
}
}
}
cout<<"最大字段和为"<<max;
减少了一层循环,时间复杂度为T(n)=O(n^2)。
2.分治算法(递归):
分治的方法关键在于将原问题拆分为相同子问题。
将原数组从中间拆分为两段(从中间分是为了尽可能减小递归深度,使算法更为高效),分别计算这两段的最大子段。这里所计算的子段包括了所有未跨这两个数组的子段,因此,剩下只需计算出跨数组的最大子段,并与两个子数组的最大子段相比较,选出最大的一个子段即为原数组最大子段。
例如数组
{
−
1
,
2
,
4
,
−
7
,
6
,
2
,
−
3
,
5
,
−
9
,
7
,
3
}
\{-1,2,4,-7,6,2,-3,5,-9,7,3 \}
{−1,2,4,−7,6,2,−3,5,−9,7,3}
分别计算子数组
{
−
1
,
2
,
4
,
−
7
,
6
,
2
}
\{-1,2,4,-7,6,2 \}
{−1,2,4,−7,6,2}和
{
−
3
,
5
,
−
9
,
7
,
3
}
\{-3,5,-9,7,3 \}
{−3,5,−9,7,3}的最大子段,这是子问题
跨数组的子段必然包括
2
2
2,
−
3
-3
−3两个元素,求最大子段,可分别向左向右依次累加。
先从2开始向左累加,
2=2, max_=2
2+6=8,max_=8
2+6-7<8,max_=8
2+6-7+4<8,max_=8
2+6-7+4+2<8,max_=8
2+6-7+4+2-1<8,max_=8
得到向左依次累加的值最大为8,向右同理。
最后考虑递归出口,即当子段只有一个元素,则返回这唯一一个元素的值。
int maxarray(int b,int e,int a[]){ //b(begin)为子段起始位置,e(end)为子段结束位置
if(b==e)return a[b];
int m1=maxarray(b,b+(e-b)/2,a); //子问题
int m2=maxarray(b+(e-b)/2+1,e,a); //子问题
int m3=-99,t1=0;
for(int i=0;i<=(e-b)/2;i++){ //左累加
t1=t1+a[b+(e-b)/2-i];
if(t1>m3)m3=t1;
}
int m4=-99,t2=0;
for(int i=0;i<=e-(e-b)/2-1-b;i++){ //右累加
t2=t2+a[b+(e-b)/2+1+i];
if(t2>m4)m4=t2;
}
int m5=m4+m3;
if(m1>=m2&&m1>=m5)return m1;
else if(m2>=m1&&m2>=m5)return m2;
else return m5; //比较得到最大子段
}
int main(){
int n=11;
int a[n] = {-1,2,4,-7,6,2,-3,5,-9,7,3};
cout<<"最大字段和为"<<maxarray(0,10,a);
return 0;
}
由以上算法可以看出时间复杂度 T ( n ) T(n) T(n)包括两个 T ( n / 2 ) T(n/2) T(n/2)的子问题和两次次数为 n / 2 n/2 n/2的循环,递归出口的时间复杂度为 1 1 1,因此可得到如下表达式:
T ( n ) = { 1 n = 1 T ( n / 2 ) + O ( n ) n > 1 T(n)=\left\{\begin{array}{rcl}1 & n=1 \\T(n/2)+O(n) & n>1 \end{array}\right. T(n)={1T(n/2)+O(n)n=1n>1
可知时间复杂度 T ( n ) = O ( n l o g ) T(n)=O(n \ log) T(n)=O(n log)。
3.动态规划
首先观察数组,如
{
−
1
,
2
,
4
,
−
7
,
6
,
2
,
−
3
,
5
,
−
9
,
7
,
1
}
\{-1,2,4,-7,6,2,-3,5,-9,7,1 \}
{−1,2,4,−7,6,2,−3,5,−9,7,1}。
它的最大子段为
{
6
,
2
,
−
3
,
5
}
\{6,2,-3,5\}
{6,2,−3,5},之所以最大,是因为该子段无论向左或者是向右添加元素,都会出现负增长,如向右合并子段
{
−
9
,
7
,
1
}
\{-9,7,1 \}
{−9,7,1},和值
−
1
-1
−1。
因此可以构造这样一种算法:自底(或自顶)依次增加元素,并计算子段和,用一个新数组记录每次增加一个元素后的新子段和,当新子段和为负数时,其他段若合并该段一定会出现负增长,因此将该段舍去,以下一个正数为起点重新计算段和,用max记录整个过程中计算的最大值和即为原数组最大子段和。
如数组 { − 1 , 2 , 4 , − 7 , 6 , 2 , − 3 , 5 , − 9 , 7 , 1 } \{-1,2,4,-7,6,2,-3,5,-9,7,1 \} {−1,2,4,−7,6,2,−3,5,−9,7,1},设记录子段和的新数组为 s s s,这里用自底增加:
子段 | 子段和 | 最大子段和 |
---|---|---|
{ 1 } \{1 \} {1} | s [ 10 ] = 1 s[10]=1 s[10]=1 | max =1 |
{ 7 , 1 } \{7,1 \} {7,1} | s [ 9 ] = 8 s[9]=8 s[9]=8 | max = 8 |
{ − 9 , 7 , 1 } \{-9,7,1 \} {−9,7,1} | s [ 8 ] = − 1 s[8]=-1 s[8]=−1(该段舍去) | max = 8 |
{ 5 } \{5 \} {5} | s [ 7 ] = 5 s[7]=5 s[7]=5 | max = 8 |
{ − 3 , 5 } \{-3,5 \} {−3,5} | s [ 6 ] = 2 s[6]=2 s[6]=2 | max = 8 |
{ 2 , − 3 , 5 } \{2,-3,5 \} {2,−3,5} | s [ , 5 ] = 4 s[,5]=4 s[,5]=4 | max = 8 |
{ 6 , 2 , − 3 , 5 } \{6,2,-3,5 \} {6,2,−3,5} | s [ 4 ] = 10 s[4]=10 s[4]=10 | max = 10 |
{ − 7 , 6 , 2 , − 3 , 5 } \{-7,6,2,-3,5 \} {−7,6,2,−3,5} | s [ 3 ] = 3 s[3]=3 s[3]=3 | max = 10 |
{ 4 , − 7 , 6 , 2 , − 3 , 5 } \{4,-7,6,2,-3,5 \} {4,−7,6,2,−3,5} | s [ 3 ] = 7 s[3]=7 s[3]=7 | max = 10 |
{ 2 , 4 , − 7 , 6 , 2 , − 3 , 5 } \{2,4,-7,6,2,-3,5 \} {2,4,−7,6,2,−3,5} | s [ 3 ] = 9 s[3]=9 s[3]=9 | max = 10 |
{ − 1 , 2 , 4 , − 7 , 6 , 2 , − 3 , 5 } \{-1,2,4,-7,6,2,-3,5 \} {−1,2,4,−7,6,2,−3,5} | s [ 3 ] = 8 s[3]=8 s[3]=8 | max = 10 |
代码:
int n=11,max=-9999;
int a[n]={-1,2,4,-7,6,2,-3,5,-9,7,1};
int s[n];
s[n-1]=a[n-1]; //自底开始
for(int i=1;i<n;i++){
if(s[n-i]>0)s[n-i-1]=s[n-i]+a[n-i-1];
else s[n-i-1]=a[n-i-1]; //大于0累加,小于0舍去
if(s[n-i-1]>max)max=s[n-i-1];
}
cout<<"最大字段和为"<<max;
不难看出,只有一层循环,时间复杂度为 T ( n ) = O ( n ) T(n)=O(n) T(n)=O(n)。