最大子序列和

这次我们来谈一谈最大子序列和问题。
问题描述如下:

给定一个长度为n的序列,其中既有正数又有负数,要求找到一个长度最少为1的子序列,使得这个子序列中所有元素的和是所有子序列中最大的。
例:3, -5, 7, -2, 8
其中最大的子序列和是7+(-2)+8=13

描述非常简单,最先想到的暴力做法是枚举子序列的位置,计算和,最后找到所有和中最大的。对于长度为 n 的序列,需要三重循环,时间复杂度为O(n3)。需要一个存储序列的数组,空间复杂度为 O(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
一开始ansMinSum都被置为sum[1](想一想如果还像原来那样,ans=-INFMinSum=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的时候,前面的序列一定不是最优解的前缀”。在本文所述的问题中,可能出现非正数序列的情况,因此这个论断不适用。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值