2019.11.2CSP模拟赛 最大K段和 双端队列xLIS问题 最大前缀和

mac2026-09-23  12

问题 A: 最大K段和

时限: 1 Sec  内存: 128 MB

题目描述

fzszkl有一个长度为N的序列A 。他希望从中选出不超过K个连续子段,满足它们两两不相交,求总和的最大值(可以一段也不选,答案为 0)。

输入格式

第一行输入两个正整数 N,K 。 第二行输入 N个整数Ai 。

输出格式

输出一行一个非负整数表示答案。

输入样例

7 2 -1 9 -2 6 -8 1 -7

输出样例

15

提示

N,M<=10^5

 

 

 

题解

明显WQS二分(虽然不知道是不是凸函数)(应该是凸函数吧)

然后把每个位置都加了一个权。。。调了1.5h,以为是自己的二分或check写错了,各种换二分姿势。。。

后来发现对每个位置加权连单调性都没有(因为增大加权可以减少段数,减小加权也可以减少段数)。。。更别说凸了

于是就。。。。。。人没了

其实应该对每一个要选的段加一个权

然后单调队列DP,又由于它没有决策点的左端的限制,所以直接记一下最大值的位置就好了,注意特判

WQS二分最重要的就是:限制什么的个数,就对什么加权(当然前提是凸函数)

代码:  

#include<cstdio> #include<cstring> #include<algorithm> using namespace std; inline int gi() { char c;int num=0,flg=1; while((c=getchar())<'0'||c>'9')if(c=='-')flg=-1; while(c>='0'&&c<='9'){num=num*10+c-48;c=getchar();} return num*flg; } #define N 100005 #define LL long long const LL INF=0x3f3f3f3f3f3f3f3fll; int a[N],g[N],n; LL sum[N],f[N]; int check(LL mid) { int mx=0; for(int i=1;i<=n;i++){ if(f[mx]+sum[i]-sum[mx]+mid>f[i-1]||(f[mx]+sum[i]-sum[mx]+mid==f[i-1]&&g[mx]+1<g[i])) f[i]=f[mx]+sum[i]-sum[mx]+mid,g[i]=g[mx]+1; else f[i]=f[i-1],g[i]=g[i-1]; if(f[i]-sum[i]>f[mx]-sum[mx]||(f[i]-sum[i]==f[mx]-sum[mx]&&g[i]<g[mx])) mx=i; } return g[n]; } int main() { int k,i,cnt=0;LL tmp=0; n=gi();k=gi(); for(i=1;i<=n;i++){ a[i]=gi(); sum[i]=sum[i-1]+1ll*a[i]; if(a[i]>0)tmp+=1ll*a[i],cnt++; } if(cnt<k){ printf("%lld",tmp); return 0; } LL l,r,mid,ans=0; l=-INF;r=0; while(l<=r){ mid=(l+r)>>1; if(check(mid)<=k){ ans=mid; l=mid+1; } else r=mid-1; } check(ans); printf("%lld",f[n]-1ll*ans*k); }

其实还有一个加强版本:https://www.luogu.org/problem/CF280D(带修改+区间查询)

这道题有一点模拟费用流的思想(就有点像可以反悔的贪心)

把选走的区间的所有数乘一个-1

然后用线段树大力维护就可以了

 

 

 

问题 B: 双端队列xLIS问题

时限: 1 Sec  内存: 128 MB

题目描述

fzszkl有N个数Ai ,他准备将他们依次插入一个双端队列(每次可以在头或尾插入一个元素),最后将 整个队列从尾到头看成一个序列,求出最长上升子序列的长度L 。他想知道, L的最大值是多少。 序列A 的一个上升子序列的严谨定义:从一个序列中删掉若干(可以为0 )个数,剩下的数满足对于任意i<j,Ai<Aj ,则称剩下的数为原序列的一个上升子序列。

输入格式

第一行输入一个正整数 N。 第二行输入N个正整数Ai 。

输出格式

输出一行一个正整数表示答案。

输入样例

8 1 9 2 6 8 1 7 1926817

输出样例

5

提示

插入时,分别选择头头尾头头尾头头,得到的序列为1,2,1,9,6,8,7,1926817,最长上升子序列为1,2,6,8,1926817 ,长度为5 。可以证明,不存在更优的方案能构造出更长的上升子序列。   对于所有数据,满足:1<=N<=10^5,1<=Ai<=10^9 

 

 

 

 

题解

首先对于一个数,从它开始的最长上升子序列是必定排在它后面的

然后观察了一下性质,发现1 9 2是永远不能排列为成1 2 9的

但是2 1 3可以排列为1 2 3

于是可以推理出:一个数,如果它后面有数比它小,则这个较小的数可以排到它的前面

但是又发现这些较小的数必须是一个递减子序列

所以答案就是从一个数开始的最长上升子序列+最长下降子序列-1

代码:  

#include<cstdio> #include<cstring> #include<algorithm> using namespace std; inline int gi() { char c;int num=0,flg=1; while((c=getchar())<'0'||c>'9')if(c=='-')flg=-1; while(c>='0'&&c<='9'){num=num*10+c-48;c=getchar();} return num*flg; } #define N 100005 int a[N],tmp[N],b[N],f[N],g[N]; int main() { int n,i,len=1; n=gi(); for(i=1;i<=n;i++) a[i]=gi(); for(i=1;i<=n;i++) tmp[i]=a[n-i+1]; len=0; for(i=1;i<=n;i++){ int j=lower_bound(b+1,b+len+1,tmp[i])-b; b[j]=tmp[i]; if(j==len+1)len++; f[i]=j; } for(i=1;i<=n;i++) tmp[i]=-a[n-i+1]; memset(b,0,sizeof(b)); len=0; for(i=1;i<=n;i++){ int j=lower_bound(b+1,b+len+1,tmp[i])-b; b[j]=tmp[i]; if(j==len+1)len++; g[i]=j; } int ans=1; for(i=1;i<=n;i++) ans=max(ans,f[i]+g[i]-1); printf("%d",ans); }

 

 

 

 

问题 C: 最大前缀和

时限: 1 Sec  内存: 128 MB

题目描述

输入格式

输入两个正整数N和M,表示1的数量和-1的数量 (N<=2000,M<=2000)

输出格式

输出答案对998244853(是个质数)取模的值。

输入样例

2 2

输出样例

5

提示

本质不同的序列有6种。  [1,1,-1,-1],最大前缀和为 2;  [1,-1,1,-1] ,最大前缀和为1 ;  [1,-1,-1,1] ,最大前缀和为1 ;  [-1,1,1,-1] ,最大前缀和为1 ;  [-1,1,-1,1] ,最大前缀和为0;  [-1,-1,1,1],最大前缀和为0. 

 

 

 

题解

先从数据范围自然想到一个O(n^3)的DP

然后又调了1h,最后发现自己有一个地方没有+1(我*……#@*¥……)!)

代码:

#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int mod=998244853; int f[205][105][105]; int n,m,ans; int cnt1,cnt_1; void dfs(int i,int mx) { if(i>n+m){ ans+=mx; if(ans>=mod)ans-=mod; return; } if(cnt1<n){ cnt1++; dfs(i+1,max(mx,cnt1-cnt_1)); cnt1--; } if(cnt_1<m){ cnt_1++; dfs(i+1,max(mx,cnt1-cnt_1)); cnt_1--; } } int main() { int i,j,k; scanf("%d%d",&n,&m); if(n<=10&&m<=10){ dfs(1,0); printf("%d",ans); } else if(n<=100&&m<=100){ f[0][0][0]=1; for(i=0;i<n+m;i++) for(j=0;j<=i;j++)if(i-j<=m&&j<=n) for(k=0;k<=j;k++){ if(!f[i][j][k])continue; if(j+1<=n)(f[i+1][j+1][max(j+1-(i-j),k)]+=f[i][j][k])%=mod; if(i+1-j<=m)(f[i+1][j][max(j-(i+1-j),k)]+=f[i][j][k])%=mod; } int sum=0; for(i=max(n-m,0);i<=n;i++){ sum+=1ll*f[n+m][n][i]*i%mod; if(sum>=mod)sum-=mod; } printf("%d",sum); } }

 

然后有因为是+1 -1,自然想到括号序列——>卡特兰数——>直线翻折

我们把+1 -1分别看成上走一步和右走一步

然后问题就转换为求点(0,0)到点(n,m)的路径,每条路径的加权为该路径上的所有点(i,j)的max(i-j,0)

点(0,0)到点(a,b)(只能右上走和右下走)且不超过直线y=x+b的方案数为:

点(0,0)到点(a,b)的所有方案 - 点(0,0)关于y=x+b的对称点到(a,b)的所有方案

于是就可以O(n)了

代码:

#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int mod=998244853; int f[205][105][105]; int n,m,ans; int cnt1,cnt_1; void dfs(int i,int mx) { if(i>n+m){ ans+=mx; if(ans>=mod)ans-=mod; return; } if(cnt1<n){ cnt1++; dfs(i+1,max(mx,cnt1-cnt_1)); cnt1--; } if(cnt_1<m){ cnt_1++; dfs(i+1,max(mx,cnt1-cnt_1)); cnt_1--; } } int jcinv[2005],jc[2005]; int ksm(int x,int y) { int ans=1; while(y){ if(y&1)ans=1ll*ans*x%mod; y>>=1;x=1ll*x*x%mod; } return ans; } void shai() { jcinv[0]=jc[1]=jc[0]=1; for(int i=2;i<=2000;i++) jc[i]=1ll*jc[i-1]*i%mod; jcinv[2000]=ksm(jc[2000],mod-2); for(int i=1999;i>=1;i--) jcinv[i]=1ll*jcinv[i+1]*(i+1)%mod; } int C(int x,int y) { return 1ll*jc[x]*jcinv[y]%mod*jcinv[x-y]%mod; } int main() { //freopen("maxpsum.in","r",stdin); //freopen("maxpsum.out","w",stdout); int i,j,k; scanf("%d%d",&n,&m); if(n<=10&&m<=10){ dfs(1,0); printf("%d",ans); } else if(n<=100&&m<=100){ f[0][0][0]=1; for(i=0;i<n+m;i++) for(j=0;j<=i;j++)if(i-j<=m&&j<=n) for(k=0;k<=j;k++){ if(!f[i][j][k])continue; if(j+1<=n)(f[i+1][j+1][max(j+1-(i-j),k)]+=f[i][j][k])%=mod; if(i+1-j<=m)(f[i+1][j][max(j-(i+1-j),k)]+=f[i][j][k])%=mod; } int sum=0; for(i=max(n-m,0);i<=n;i++){ sum+=1ll*f[n+m][n][i]*i%mod; if(sum>=mod)sum-=mod; } printf("%d",sum); } else{ shai(); int ans=0,pre=0; for(i=n-m;i<=n;i++){ int t=((C(n+m,n)-C(n+m,i+m+1))%mod-pre)%mod; if(i>0)ans=(1ll*ans+1ll*t*i)%mod; pre=(1ll*pre+1ll*t)%mod; } printf("%d",ans); } }

 

 

 

 

 

 

 

 

 

 

 

最新回复(0)