这题乍一看还挺简单的,仔细一看,好像还有那么点意思。
题意:
从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状态定义和转移,以及优化的思考过程,还可以好好的挖掘~

京公网安备 11010502036488号