最大子序列和
1. 题目/题目地址
https://www.acwing.com/problem/content/1481/

2. 题目解析

这道题我们从两种角度来考虑:
1. 状态表示f[i]:所有以i为右端点的子序列。
状态属性f[i]:这些子序列的最大值
2. 状态计算:我们将这个集合分成两个部分:
2.1 只有一个数(i,i)-> w[i],最大值就是w[i]。
2.2 多于一个数(i-1,i)、(i-2,i-1,i)、...、(1,2,...,i)
在这些子序列中,均有w[i]。因此,最大值不在于w[i],而在于前面的这些数。我们可以发现,前面的这些数,刚好就是f[i-1]。因此,最大值:f[i-1] + w[i]
3. 因此:f[i] = max(w[i],f[i-1] + w[i])
4. 我们将所有的f[i]求出来,再求一个最大值,这就是本问题的解。(可以参考第一步的代码)
5. 我们如何求出最大值的子序列呢?-> 找到子序列的起点下标和终点下标。
我们将f[i] = max(w[i],f[i-1] + w[i])进行等价变形 -> f[i] = w[i] + max(0,f[i-1])
6. 对于f[i] = w[i] + max(0,f[i-1])我们可以做如下解释:
6.1 如果f[i-1] < 0, f[i] = w[i]。这就意味着:如果以i-1结尾的所有子序列的最大值小于0,那么就舍弃(将f[i-1] = 0即可)。
6.2 如果f[i-1] > 0, f[i] = w[i] + f[i-1]。这就意味着:如果以i-1结尾的所有子序列的最大值大于0,那么将当前的数(w[i]),纳入到这个子序列中。
6.3 我们如何在计算当中,求得最大值子序列的起始下标和终点下标?
记录一个变量start,表示起始位置。(终点位置就是i,具体看状态表示)
记录两个变量l和r,表示最大子序列的起始位置和终点位置。
对于情况6.1:start = i 即可。
对于情况6.2:start不变即可。
如果当前子序列的值大于res(本题答案),那么更新res为当前子序列的值。并且: l = start,r = i。(参考代码的第二步)
7. 求得了l和r之后,我们就可以遍历,进而得到最大值子序列(参考代码的第三步)。
8. 需要注意:在上述的递推公式中,f[i]只用到了f[i-1]。因此,我们可以开一个变量f,代表f[i-1],此时就不用开一个数组了。
9. res从-1开始,因为:如果所有数字均为负数,那么res的值处理完后恒为-1,用于处理例外情况。
10. f也从-1开始,因为:我们要给start赋初值。
3. 题解
// 第一步:求得这些子序列中最大的和
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 10010;
int w[N];
int n;
int main(){
scanf("%d",&n);
for(int i = 0; i < n; i ++){
scanf("%d",&w[i]);
}
int res = -1,l,r;
// f 指的是f[i-1]
for(int i = 0, f = -1,start; i < n; i ++){
f = w[i] + max(0,f); // 等价于: f[i] = w[i] + max(0,f[i-1])
res = max(res,f);
}
cout << res << endl;
return 0;
}
// 第二步:找到和最大的子序列(起点和终点)
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 10010;
int w[N];
int n;
int main(){
scanf("%d",&n);
for(int i = 0; i < n; i ++){
scanf("%d",&w[i]);
}
int res = -1,l,r;
for(int i = 0, f = -1,start; i < n; i ++){
if(f < 0){
f = 0;
start = i;
}
f += w[i];
if(f > res){
res = f;
l = start;
r = i;
}
}
if(res < 0){
cout << 0 << " " << w[0] << " " << w[n-1] << endl;
}else{
cout << res << " " << w[l] << " "<< w[r] << endl;
}
return 0;
}
// 第三步:输出和最大的子段
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 10010;
int w[N];
int n;
int main(){
scanf("%d",&n);
for(int i = 0; i < n; i ++){
scanf("%d",&w[i]);
}
int res = -1,l,r;
for(int i = 0, f = -1,start; i < n; i ++){
if(f < 0){
f = 0;
start = i;
}
f += w[i];
if(f > res){
res = f;
l = start;
r = i;
}
}
if(res < 0){
cout << 0 << " " << w[0] << " " << w[n-1] << endl;
}else{
cout << res << " " << w[l] << " "<< w[r] << endl;
// 输出和最大的子段
for(int i = l; i <= r; i ++){
printf("%d ", w[i]);
}
}
return 0;
}

4. 参考资料
[1] 上图的课件来自于中国科学院大学马丙鹏老师的计算机算法设计与分析课程。
[2] 本题思路参考https://www.acwing.com