这题乍一看还挺简单的,仔细一看,好像还有那么点意思。

题意:
从n个数中选出两个不相交的区间,且区间长度都为K,求这两个区间的和的最大值。(n<=200000)

题目类型:
前缀和、动态规划、线段树

思路:
由于区间长度是固定的,很容易想到枚举区间的起点。如果分别枚举两个区间的起点的话,我们需要O(n^2)复杂度,中间的求和可以通过预处理前缀和获得,但是显然会超时。
我一开始的做法是,预处理出所有长度为K的区间和,然后丢进线段树里。接着枚举其中一个区间的起点,那么剩下的一个区间的最大值直接从线段树里去查就好了,这样的话O(nlogn)就可以解决。嗯,写起来还是比较快的。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=200040;
const ll inf=3e10;
int n,k;
ll a[maxn],sum[maxn],sumk[maxn];   //sum[]表示前缀和,sumk[]表示长度为k的区间和
void init(){
    sum[0]=0;
    for(int i=1;i<=n;i++)sum[i]=sum[i-1]+a[i];
    for(int i=1;i<=n-k+1;i++){
        sumk[i]=sum[i+k-1]-sum[i-1];
    }
}
struct node{
    int l,r;
    ll maxi;
}tree[maxn<<2];
void build(int l,int r,int rt){
    tree[rt].l=l;
    tree[rt].r=r;
    if(l==r){
        tree[rt].maxi=sumk[l];
        return;
    }
    int mid=l+r>>1;
    build(l,mid,rt<<1);
    build(mid+1,r,rt<<1|1);
    tree[rt].maxi=max(tree[rt<<1].maxi,tree[rt<<1|1].maxi);
}
ll query(int l,int r,int rt){
    if(l>r)return -inf;
    if(l==tree[rt].l&&r==tree[rt].r){
        return tree[rt].maxi;
    }
    int mid=tree[rt].l+tree[rt].r>>1;
    if(mid>=r)return query(l,r,rt<<1);
    else if(l>mid)return query(l,r,rt<<1|1);
    else return max(query(l,mid,rt<<1),query(mid+1,r,rt<<1|1));
}
int main()
{
    int t;
    cin>>t;
    while(t--){
        cin>>n>>k;
        for(int i=1;i<=n;i++)cin>>a[i];
        init();
        build(1,n-k+1,1);
        ll maxians=-inf;
        for(int i=1;i<=n-k+1;i++){
            ll b1,b2;
            b1=sumk[i];
            b2=max(query(1,i-k,1),query(i+k,n-k+1,1));
            maxians=max(maxians,b1+b2);
        }
        cout<<maxians<<endl;
    }
    return 0;
}

题解中提供了一种线性做法,代码更加简洁而且速度更快。
我们可以将两个区间分成左右两个,我们可以从右往左枚举左边区间的起点,右边区间的起点我们可以不需要枚举了,直接在过程中进行维护就可以。
根据这个思路,我写了一个类似尺取法的代码。过程中维护右边区间的最大和。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=200040;
const ll inf=3e10;
int n,k;
ll a[maxn],sum[maxn],sumk[maxn];
void init(){
    sum[0]=0;
    for(int i=1;i<=n;i++)sum[i]=sum[i-1]+a[i];
    for(int i=1;i<=n-k+1;i++){
        sumk[i]=sum[i+k-1]-sum[i-1];
    }
}

int main()
{
    int t;
    cin>>t;
    while(t--){
        cin>>n>>k;
        for(int i=1;i<=n;i++)cin>>a[i];
        init();
        ll maxians=sumk[n-2*k+1]+sumk[n-k+1];
        //cout<<"*******"<<maxians<<endl;
        int l,r;
        l=n-k;r=n;
        ll t1,t2,tmp;
        t2=tmp=sumk[n-k+1];
        for(int i=n-2*k;i>=1;i--){
            t1=sumk[i];
            t2=max(t2,tmp=tmp-a[r--]+a[l--]);
            maxians=max(maxians,t1+t2);
        }
        cout<<maxians<<endl;
    }
    return 0;
}

当然也可以用一个数组来记录,令maxr[i]表示从i开始的所有长度为K的区间中的最大和。
那么我们可以得到一个转移方程,maxr[i]=max(maxr[i+1],sum[i+k-1]-sum[i-1])。这样一来,线性时间就解决了。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=200040;
const ll inf=3e10;
int n,k;
ll a[maxn],sum[maxn],sumk[maxn],maxr[maxn];
void init(){
    sum[0]=0;
    for(int i=1;i<=n;i++)sum[i]=sum[i-1]+a[i];
    for(int i=1;i<=n-k+1;i++){
        sumk[i]=sum[i+k-1]-sum[i-1];
    }
}

int main()
{
    int t;
    cin>>t;
    while(t--){
        cin>>n>>k;
        for(int i=1;i<=n;i++)cin>>a[i];
        init();
        ll maxians=-inf;
        maxr[n-k+1]=sum[n]-sum[n-k];
        for(int i=n-k;i>=1;i--){
            maxr[i]=max(maxr[i+1],sum[i+k-1]-sum[i-1]);
        }
        for(int i=1;i<=n-2*k+1;i++){
            ll t1,t2;
            t1=sumk[i];
            t2=maxr[i+k];
            maxians=max(maxians,t1+t2);
        }
        cout<<maxians<<endl;
    }
    return 0;
}

接着,题解中提出了3个变式,都是值得思考。

给大家提出几个变形的相关问题:
一:不限制长度——在一个数列里找两个不相交区间使得他们权值和最大
二:区间数目变多——找 m个长度为 k 的不相交区间使得他们的权值和最大 ( 1≤n≤5000)
三:区间数目变多且不限制长度——找 m 个不相交长度不限的区间使得他们权值和最大( 1≤n≤5000)

首先是变形一:
利用原题的思路,我们可以得到maxr[i]表示从i开始往右的区间和中的最大值,同样的我们可以用maxl[i]表示从i开始往左的区间和中的最大值。然后要找两个不相交区间的权值和最大,只要枚举左右两个区间的分界点就好了。复杂度也是O(n)。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=5040;
const ll inf=3e10;
int n,k;
ll a[maxn],sum[maxn],maxl[maxn],maxr[maxn],dp1[maxn],dp2[maxn];
void init(){
    sum[0]=0;
    for(int i=1;i<=n;i++)sum[i]=sum[i-1]+a[i];

}

int main()
{
    int t;
    cin>>t;
    while(t--){
        cin>>n>>k;
        for(int i=1;i<=n;i++)cin>>a[i];
        init();
        for(int i=1;i<=n;i++){
            dp1[i]=max(dp1[i-1]+a[i],a[i]);
        }
        for(int i=n;i>=1;i--){
            dp2[i]=max(dp2[i+1]+a[i],a[i]);
        }
        for(int i=n;i>=1;i--){
            maxr[i]=max(maxr[i+1],dp2[i]);
        }
        for(int i=1;i<=n;i++){
            maxl[i]=max(maxl[i-1],dp1[i]);
        }
        ll maxians=0;
        for(int i=2;i<n;i++){
            maxians=max(maxians,max(maxl[i]+maxr[i+1],maxl[i-1]+maxr[i]));
        }
        cout<<maxians<<endl;
    }
    return 0;
}

变形二:
现在需要找m个区间长度为K的不相交区间的最大权值和。
这个问题其实就是分组问题,令dp[i][j]表示前i个数已经分了j组的最大权值和。
对于当前第i个数,若选择他,注意我们在进行dp维护的时候要保证已经分了j组了,所以我们可以获得的转移方程式:dp[i][j]=dp[i-k][j-1]+sum[i]-sum[i-k]。也就是说,a[i]刚好成为j组的最后一个,那么我们自然从dp[i-k][j-1]转移过来了。
若不选择a[i],显然我们有dp[i][j]=dp[i-1][j]
因此两个里面取大的那个就好了。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=5040;
const ll inf=3e10;
int n,k,m;
ll a[maxn],sum[maxn],dp[maxn][maxn];
void init(){
    sum[0]=0;
    for(int i=1;i<=n;i++)sum[i]=sum[i-1]+a[i];
    for(int i=0;i<maxn;i++){
        for(int j=0;j<maxn;j++)dp[i][j]=-inf;
    }
    for(int i=0;i<maxn;i++)dp[i][0]=0;
}

int main()
{
    int t;
    cin>>t;
    while(t--){
        cin>>n>>m>>k;   //n个数m组,每组k个
        for(int i=1;i<=n;i++)cin>>a[i];
        init();
        for(int i=1;i<=n;i++){
            for(int j=1;j<=min(m,i/k);j++){
                dp[i][j]=max(dp[i-1][j],dp[i-k][j-1]+sum[i]-sum[i-k]);
            }
        }
        //for(int i=1;i<=n;i++)cout<<dp[i][m]<<endl;
        cout<<dp[n][m]<<endl;
    }
    return 0;
}
/*
2
8 3 2
1 -1 2 3 4  -9 8  5
6 2 2
2 3 -4 5 7 -8
*/

变形三:
如果不限长度,不限数量。
参考变形二,我们可以得到dp[i][j]表示前i个数分j组的最大和。
显然,此刻我们按照选或不选的方式去转移,选择a[i]的时候,我们发现无法进行转移了:因为长度不确定,所以无法判断从哪里过来了。
这里我们修改一下dp状态,令dp[i][j]表示前i个数分j组并且i强制选择的最大和。
此时我们可以知道,dp[i][j]有两种情况,第一种是和前面连续,dp[i][j]=dp[i-1][j]+a[i]
第二种是a[i]单独一个新区间,dp[i][j]=dp[k][j-1]+ai
好了dp[k][j-1]中的k是需要枚举的,但我们可以发现k和i时同步增加的,因此我们维护一个maxp[i][j]前缀和数组,表示1-i中分j组的最大值。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=5040;
const ll inf=3e10;
int n,k,m;
//maxp[i]表示1-i中分j组的最大值 
ll a[maxn],sum[maxn],dp[maxn][maxn],maxp[maxn][maxn];
void init(){
    sum[0]=0;
    for(int i=1;i<=n;i++)sum[i]=sum[i-1]+a[i];
    for(int i=0;i<maxn;i++){
        for(int j=0;j<maxn;j++)dp[i][j]=maxp[i][j]=-inf;
    }
    for(int i=0;i<maxn;i++)dp[i][0]=0;
}

int main()
{
    int t;
    cin>>t;
    while(t--){
        cin>>n>>m;   //n个数m组,每组不限 
        for(int i=1;i<=n;i++)cin>>a[i];
        init();
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                maxp[i-1][j-1]=max(maxp[i-2][j-1],dp[i-1][j-1]);
                dp[i][j]=max(dp[i-1][j]+a[i],maxp[i-1][j-1]+a[i]);
            }
        }
       // cout<<maxp[1][1]<<' '<<maxp[2][1]<<endl;
       ll maxians=-inf;
        for(int i=1;i<=n;i++)maxians=max(maxians,dp[i][m]);
        cout<<maxians<<endl;
    }
    return 0;
}
/*
2
8 3 2
1 -1 2 3 4  -9 8  5
6 2 2
2 3 -4 5 7 -8
*/

更进一步地,可以将dp状态定义为dp[i][j][0/1]表示前i个分成j组且第i个取或不取的状态。
这样一来,状态转移更加清晰:
dp[i][j][0]=max(dp[i-1][j][0],dp[i-1][j][1])
dp[i][j][1]=max(dp[i-1][j][1],dp[i-1][j-1][1],dp[i-1][j-1][0])+a[i]

果然很多时候dp多一维状态表示可以简化很多思考过程和转移过程。
这中间的dp状态定义和转移,以及优化的思考过程,还可以好好的挖掘~