这次我们来谈一谈最大子序列和问题。
问题描述如下:
给定一个长度为n的序列,其中既有正数又有负数,要求找到一个长度最少为1的子序列,使得这个子序列中所有元素的和是所有子序列中最大的。
例:3, -5, 7, -2, 8
其中最大的子序列和是7+(-2)+8=13
描述非常简单,最先想到的暴力做法是枚举子序列的位置,计算和,最后找到所有和中最大的。对于长度为
n
的序列,需要三重循环,时间复杂度为
int a[MAXN];
for (int i = 1; i <= n; ++i) cin >> a[i]; //读入
int ans = -INF; //维护子序列和的最大值
for (int i = 1; i <= n; ++i) //子序列的起点
for (int j = i; j <= n; ++j) {//子序列的终点
int sum = 0; //当前子序列和
for (int k = i; k <= j; ++k) sum += a[k]; //计算子序列和
if (ans < sum) ans = sum; //维护最大值
}
//最终结果为ans
但是,如此高的时间复杂度无法让人满意。我们来做一点简单的优化:前缀和。原理很简单,用一个新的数组sum
来记录前缀和,其中
sum[k]=∑ki=1a[i]
。利用前缀和的性质
sum[r]−sum[l]=∑ri=l+1a[i]
,我们可以减少一层循环,把计算子序列和的时间复杂度降到
O(1)
,从而把总的时间复杂度降到
O(n2)
。空间复杂度不变,仍然是
O(n)
。实现如下:
int a[MAXN], sum[MAXN] = {0};
for (int i = 1; i <= n; ++i){
cin >> a[i]; //读入
sum[i] = sum[i - 1] + a[i]; //计算前缀和
}
int ans = -INF; //维护子序列和的最大值
for (int i = 1; i <= n; ++i) //子序列的起点
for (int j = i; j <= n; ++j) //子序列的终点
ans = max(ans, sum[j] - sum[i - 1]);
//最终结果为ans
那么我们能不能继续优化呢?只要还采用枚举子序列起点终点的办法,时间复杂度至少就是%O(n^2)%。然而,我们可以采用动态规划的思想。
依然用ans
来维护当前子序列和的最大值,同时用MinSum
来记录已经扫过的子序列和的最小值。这样,我们可以得到状态转移方程:
ans=max{ans,sum[i]−MinSum}
同时维护MinSum的值:
MinSum=min{MinSum,sum[i]}
这样只需要一遍循环,时间复杂度为
O(n)
。由于需要存储前缀和,空间复杂度还是
O(n)
。实现如下:
int sum[MAXN] = {0};
for (int i = 1; i <= n; ++i){
cin >> sum[i]; //读入
sum[i] = sum[i - 1] + a[i]; //计算前缀和
}
//注意:这里把a和sum合成为一个数组,因为a在状态转移方程中没有出现
int ans = sum[1]; //维护子序列和的最大值
int MinSum = sum[1]; //已经扫过的子序列和的最小值
for (int i = 2; i <= n; ++i){
ans = max(ans, sum[i] - MinSum);
MinSum = min(MinSum, sum[i]); //更新最小值
}
//最终结果为ans
这里结合一开始给出的数据3, -5, 7, -2, 8
作简单的推演。
数据的前缀和数组为:3, -2, 5, 3, 11
。
一开始ans
和MinSum
都被置为sum[1]
(想一想如果还像原来那样,ans=-INF
,MinSum=INF
并且第一次循环i=1
会有什么后果),第一次循环i=2。这时sum[i]-MinSum=-5>ans,ans
不更新。但MinSum
更新为-2,由3+(-5)得到。
第二次循环i=3。sum[i]-MinSum=7>3=ans,ans
更新为7,当前最大和是a[3]=7(虽然没有数组a,为了方便,我们仍然用a[i]来表示第i个数)。MinSum
不更新。
第三次循环i=4。sum[i]-MinSum=5<7=ans,ans
不更新。MinSum
也不更新。这表示,前四个数中,最大子序列和为a[3]=7。
第四次循环i=5。sum[i]-MinSum=13>7=ans,ans
更新为13,由a[3]+a[4]+a[5]得到。MinSum
不更新。
这样循环结束,得到最终答案13。
到这里,因为仅仅读入数据就是
O(n)
,时间复杂度已经不能再降低。但空间复杂度仍然可以优化。注意到每次只用到了sum[i]
,那么边读入边处理是一个很好的选择。实现如下:
int a; //相当于原来的a[i]
int sum = a; //前缀和
int ans = a; //维护子序列和的最大值
int MinSum = a; //已经扫过的子序列和的最小值
for (int i = 1; i <= n; ++i){
cin >> a;
sum += a; //计算前缀和
ans = max(ans, sum - MinSum);
MinSum = min(MinSum, sum); //更新最小值
}
//最终结果为ans
至此,我们已经找到了最大子序列和问题的最优解:时间复杂度 O(n) ,空间复杂度 O(1) 。
最后说一下,有些文章中写“当sum<0的时候,前面的序列一定不是最优解的前缀”。在本文所述的问题中,可能出现非正数序列的情况,因此这个论断不适用。