题目描述:
最大连续子序列和世人皆知,现在给定一个序列,求删除连续的一段之后(也可以不删除)的最大连续子序列和。
例如: 1 2 -4 3 3 -4 4 -5 4 -3
删除中间的【-4 4 -5】后最大连续子序列是3 3 4。其最大连续子序列和为10。
分析:
最大连续子序列可以用动态规划很简单的就求出来。
定义f[i]表示以a[i]结尾的最大连续子序列的和。
则f[i]=max(f[i-1]+a[i],a[i]);
如果条件没有删除的话,我们现在已经求出了传统的最大连续子序列的和,就是在所有的f[i]中选择最大的。
怎么样利用这个基础的f[i],来求出删除一段后的最大连续子序列呢?
删除一段后的最大子序列的和=删除部分的左边的最大子序列的和+删除部分的右边的最大子序列的和
而删除部分的左边的最大子序列的和恰好可以从f[i]中查询到,右边的,我们只需要构造一个反方向的类似于f[i]功效的数组g[i]即可。
定义g[i]代表从n倒叙到i的最大连续子序列的和。
则g[i]=max(g[i+1]+a[i],a[i]);
有了f[i]和g[i]两个数组,我们就可以枚举删除部分的边界k了。
假设边界是k,我们只需要找出1..k中最大的f[i]以及k+1...n中最大的g[j],然后两个相加就是边界k的所求的最大连续子序列的和。
用样例来说明下:
初始数据: 1 2 -4 3 3 -4 4 -5 4 -3
从1到i的最大和f[i]: 1 3 -1 3 6 2 6 1 5 2
从n到i的最大和g[i]:5 4 2 6 3 0 4 -1 4 -3
假设枚举的边界k是8,则8左边(包含k)最大的f[i]是f[5]=6或者f[7]=6,边界8右边最大的g[i]是g[9]=4
把左右两边最大的都加起来就求得了以边界k=8情况下的最大值。
代码:
#include<iostream>
using namespace std;
int a[100];
int f[100],g[100];
int n,ans;
int main(){
cin>>n;
for (int i=1; i<=n; i++)
cin>>a[i];
for (int i=1; i<=n; i++){
f[i]=max(f[i-1]+a[i],a[i]);
}
for (int i=n; i>=1; i--){
g[i]=max(g[i+1]+a[i],a[i]);
}
int Lmax=f[1];
for (int i=1; i<=n; i++){
if (f[i]>Lmax) Lmax=f[i];
int Rmax=g[i+1];
for (int j=i+1; j<=n; j++)
Rmax=max(Rmax,g[j]);
ans=max(ans,Lmax+Rmax);
//cout<<i<<" "<<Lmax<<" "<<Rmax<<endl;
}
cout<<ans<<endl;
}
优化:
有了f[i] g[i],在求最后的匹配时,上面的方法用了两重循环,复杂度n^2。
其实也可以提前初始化好一个数组,用来查询从i...n的最大值是多少,使之时间复杂度降为O(n)。
定义 s[i]表示从i....n的区间中最大的g[i]。s[i]这个数组的作用就代替了上面的求Rmax的j的循环。
代码:
#include<iostream>
using namespace std;
int a[100];
int s[100];
int f[100],g[100];
int n,ans;
int main(){
cin>>n;
for (int i=1; i<=n; i++)
cin>>a[i];
for (int i=1; i<=n; i++){
f[i]=max(f[i-1]+a[i],a[i]);
}
for (int i=n; i>=1; i--){
g[i]=max(g[i+1]+a[i],a[i]);
}
s[n]=g[n];
for (int i=n-1; i>=1; i--){
s[i]=max(s[i+1],g[i]);
}
int Lmax=f[1];
for (int i=1; i<=n; i++){
if (f[i]>Lmax) Lmax=f[i];
ans=max(ans,Lmax+s[i+1]);
//cout<<i<<" "<<Lmax<<" "<<s[i+1]<<endl;
}
cout<<ans<<endl;
}
总结:
这个删除部分求最大连续子序列的和,让我想到了凸型的合唱队形那个经典题目。
他们都是把问题分割为两部分,再由两部分拼接成最终的答案。
子序列的问题,通常用动态规划来求解,正序倒序,以及分段求解都是通用技能。