codevs 3327 选择数字
3327 选择数字
时间限制: 1 s
空间限制: 256000 KB
题目描述 Description
给定一行n个非负整数a[1]..a[n]。现在你可以选择其中若干个数,但不能有超过k个连续的数字被选择。你的任务是使得选出的数字的和最大。
输入描述 Input Description
第一行两个整数n,k
以下n行,每行一个整数表示a[i]。
输出描述 Output Description
输出一个值表示答案。
样例输入 Sample Input
5 2
1
2
3
4
5
样例输出 Sample Output
12
数据范围及提示 Data Size & Hint
对于20%的数据,n <= 10
对于另外20%的数据, k = 1
对于60%的数据,n <= 1000
对于100%的数据,1 <= n <= 100000,1 <= k <= n,
0 <= 数字大小 <= 1,000,000,000
第一步:dfs
20分 TLE
#include<cstdio> #include<algorithm> using namespace std; int n,k; long long ans; long long a[100001]; void dfs(int now,long long sum,int con) { if(now==n) { ans=max(ans,sum); return; } for(int i=now+1;i<=n;i++) { if(con<k) dfs(i,sum+a[i],con+1); dfs(i,sum,0); } } int main() { scanf("%d%d",&n,&k); for(int i=1;i<=n;i++) scanf("%d",&a[i]); dfs(0,0,0); printf("%lld",ans); }
这种写法好像不能改记忆化搜索
第二步:朴素的DP
唉,这一步也想不出来
看的那篇题解里写着一句话:人傻就要多做题
90分TLE 应该是数据弱。。。
不能有超过连续的k个数字被选择,所以第i个状态可以由第i-k到i的状态更新而来
定义f[i]表示选到第i个数字的最大和
在i-k——i这段长为k+1的序列中,必须有一个断点
枚举断点j
状态转移方程:f[i]=max(f[j-1]+sum[i]-sum[j]) i-k<=j<=i
#include<cstdio> #include<algorithm> using namespace std; int n,k; long long a[100001],f[100001],sum[100001]; int main() { scanf("%d%d",&n,&k); for(int i=1;i<=n;i++) scanf("%lld",&a[i]),sum[i]=sum[i-1]+a[i]; for(int i=1;i<=k;i++) f[i]=f[i-1]+a[i]; for(int i=k+1;i<=n;i++) for(int j=i-k;j<=i;j++) f[i]=max(f[i],f[j-1]+sum[i]-sum[j]); printf("%lld",f[n]); }
开始做的时候 j只枚举到i-1,忽略了选i-k——i-1这段长为k的序列的情况
第三步:
单调队列优化DP
观察状态转移方程,交换后两项顺序:f[i]=max(f[j-1]-sum[j]+sum[i])
当i固定式,sum[i]固定,所以f[i]只与j有关
所以可以用一个单调递减的队列维护f[j-1]-sum[j]的最大值,
f[i]=队首(f[j-1]-sum[j]的最大值)+sum[i]
#include<cstdio> #include<algorithm> using namespace std; int n,k,head,tail; long long a[100001],f[100001],sum[100001],d[100001],q[100001]; int main() { scanf("%d%d",&n,&k); for(int i=1;i<=n;i++) scanf("%lld",&a[i]),sum[i]=sum[i-1]+a[i]; for(int i=0;i<=n;i++) { d[i]=f[i-1]-sum[i]; while(head<tail&&q[head]<i-k) head++; while(head<tail&&d[i]>d[q[tail-1]]) tail--; q[tail++]=i; f[i]=d[q[head]]+sum[i]; } printf("%lld",f[n]); }