01-1. 最大子列和问题(20)
时间限制 10000 ms
内存限制 65536 kB
代码长度限制 8000 B
判题程序
Standard
给定K个整数组成的序列{ N1, N2, …, NK },“连续子列”被定义为{ Ni, Ni+1, …, Nj },其中 1 <= i <= j <= K。“最大子列和”则被定义为所有连续子列元素的和中最大者。例如给定序列{ -2, 11, -4, 13, -5, -2 },其连续子列{ 11, -4, 13 }有最大的和20。现要求你编写程序,计算给定整数序列的最大子列和。
输入格式:
输入第1行给出正整数 K (<= 100000);第2行给出K个整数,其间以空格分隔。
输出格式:
在一行中输出最大子列和。如果序列中所有整数皆为负数,则输出0。
输入样例:
6 -2 11 -4 13 -5 -2
输出样例:
20
提交地址:http://www.patest.cn/contests/mooc-ds/01-1
美好的寒假开始了,不做题觉得没事干,正好手上有一套浙大数据结构的练习题,就先把它刷了吧~
这个是上来第一题,中文,题意就没必要说了。
分析:
1、马上想到暴力,既然是最大连续子列和,那么用一个二重循环可以枚举子列头和尾的位置,内层再做一个求和的扫描,O(n^3)的办法马上就有了,而且写起来很简单。不过显然这个要超时…
2、其实暴力也是可以优化的,内层扫描那一遍完全可以省掉。方法是记一个数组sum,sum[i]表示从0到i号元素的和,这个可以在一开始O(n)预处理出来,然后到后面确定头i尾j之后,直接sum[j] – sum[i]就可以O(1)得出i到j的元素和,这样一来复杂度降到O(n^2),水题的话到这就够用了。
3、但是只会暴力是不行的,不然还要算法干什么。这个题目明显有优化的空间,因为能产生最大子列和的子列必然满足一些特点,也就是说有些子列是可以直接pass的,连试都不用去试它:
(1)如果全是非负数,那直接求全部元素的和,就是最大子列和。
(2)如果全是非正数,那问题等价为求数列中最大元素的问题,因为这种情况下,选取的子列越长,和就被减得越小。
注意到上面两种情况都是O(n)的,所以我们看看能不能让下一种情况也在O(n)内解决。
(3)如果数列有正有负,这时就需要思考一下最后选取的子列有什么特征:
①子列肯定不能以负数开头和结尾。如果以负数开头和结尾我们还不如不要它,因为不要它我们还可以少减一个数,让子列和更大。
②子列和必定是正数。既然是有正有负的数列,实在不行我可以取某一个正数构成长度为1的子列,它的子列和至少是个正数,那些和是负数或0的子列明显不如它。
③如果一个子列的和是负数,则最终有最大和的子列不会以这个子列为两端,这与第①条是同理的。
4、由以上性质可以得到一种很巧妙的方法:从左到右扫描一遍数组,记一个变量tmp求数组元素和,如果tmp在某时刻小于0,就把tmp置0,然后继续。再用一个变量ans记录tmp的最大值即可。
这个方法乍看上去感觉不太对的样子,但是仔细想想它完全符合上面①②③三条性质。简单的说,如果一个子列的左侧有小于0的子列,那tmp会自动置0来保护能取到最优,如果右侧有小于0的子列,但是由于是从左往右扫描的,那时候ans早已记录到了最大值,tmp减小并不会使结果错误,如果后面又有和为正数的子列,tmp还能机智的把前面这一块和为正数的子列加上去,尝试得到和更大的子列。
容易发现上面这种方法只有O(n)的时间复杂度,代码也非常容易实现,且(1)(2)两种情况可以完美整合进(3)的实现。美中不足的是要想同时求子列的首尾位置就不如暴力法方便了,需要额外打标签判断。
暴力的代码就不写了,这种O(n)方法的代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <iostream>
#include <algorithm>
using namespace std;
#define MAXN 100010
int a[MAXN];
int main()
{
int n;
cin >> n;
for(int i=0; i<n; i++) scanf("%d", &a[i]);
int ans = a[0], tmp = 0;
for(int i=0; i<n; i++){
tmp += a[i];
if(tmp > ans) ans = tmp;
else if(tmp < 0){
tmp = 0;
}
}
cout << ans << endl;
return 0;
}