最大子序列和

来源:互联网 发布:企业即时聊天软件 编辑:程序博客网 时间:2024/05/18 18:56

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

给定一个长度为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的时候,前面的序列一定不是最优解的前缀”。在本文所述的问题中,可能出现非正数序列的情况,因此这个论断不适用。

原创粉丝点击