1007 Maximum Subsequence Sum (25 分)
今天介绍一道PAT甲级的小题,最长子列和的变形
简单翻译:
给你一个数列,输出这个数列最大的字列和,以及这个和的第一个元素和最后一个元素。
思路:
首先介绍最大子列和问题,有很多种做法
- 暴力遍历 O(n三次方)
- 前缀和 O(n平方)
- 一次遍历 O(n)
这里介绍第三种方式。
因为这个子列是连续的。首先考虑一种情况:
- 存在连续的多个正数:此时最优子列的最边界一定在最左侧,因为子列的长度没有限制,如果在右边的某个位置得到一个很大的子列和,那么左边界往左拓展一点,一定会更大,所以左边界一定在最左侧
理解了这一点,那么我们在确定左边界的时候,就可以使用贪心的策略了,这里就可以得出一个结论,左边界只可能在最左边的正元素上。
例如,数列5 2 3 -1 2 8 -2 -3 -5 -10 3 20中,左边界只有可能是这些加粗的点。其他的点当最优解的左边界都有更好的选择。
看到这里这个算法已经理解了精髓部分了。我们具体的策略是:
- 从左往右,找到第一个可能的左边界(即第一个正数)
- 现在开始边往后遍历边求和,并维护当前的最大子列和
- 如果某次求和后,当前的sum小于0了,这时候果断抛弃这个左边界,并将sum初始化为0,寻找下一个可能的左边界。重复此过程。
小问题:
如果只看这个描述,这个算法还会出现另外一个问题,因为我们在求和的过程中,可能会把一些可能的左边界给遗忘掉,例如上面数列的例子中,在计算第一个左边界的时候,第二个可能的左边界2,就会直接被跳过,这样不会导致错误吗?答案是不会的,因为在计算到这个2的时候,前面的和已经是一个正数了,把2前面的-1给覆盖掉了,这就相当于在2之前重新放了一个正数,这样2已经不是一个待选的可能左边界了,所以这样处理是没有问题的。
解决方案:
有了上述关于最大子列和问题的讨论,这个问题也就迎刃而解了。这里只需要多两个变量来保存这个子列的第一个元素和最后一个元素即可。
这个问题还留有一个小坑,那就是整个数列可能全部都是负数,这时题目规定这种情况下,最大子列和为0,输出数列的第一个元素和最后一个元素即可。
C++代码:
#include<bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
int start = 0;
int realS = 0;
int end = 0;
int realE = 0;
int m = -INT_MAX;
int temp = 0;
for (int i = 0; i < n; i++){
temp += a[i];
end = i;
if (temp > m){
m = temp;
realS = start;
realE = end;
}
if (temp < 0) {
temp = 0;
start = i + 1;
end = i + 1;
}
}
if (m < 0) {
cout << 0 << " " ;
cout << a[0] << " " << a[n - 1];
} else {
cout << m << " ";
cout << a[realS] << " " << a[realE];
}
}