【单调队列优化dp】 分组
>>>>题目
【题目】
给定一行n个非负整数,现在你可以选择其中若干个数,但不能有连续k个数被选择。你的任务是使得选出的数字的和最大
【输入格式】
第一行两个整数 n,k,如题目描述
接下来一行n 个数,表示这个序列
【输出格式】
输出一行一个数,表示最大的和
【输入样例】
5 2
1 2 3 4 5
【输出样例】
12
【数据范围与约定】
对于20%的数据,保证1 <=n <=10。
对于40%的数据,保证1 <=n <=200。
对于60%的数据,保证1 <=n <=100000。
对于100%的数据,保证1 <=n <=2000000,1<=K<=n。
>>>>分析
因为题目给出非负整数,根据贪心,我们尽量多选择长度为k的序列
数据范围很大,暴力求每一段的和的做法肯定会T掉,那么考虑dp
题目涉及到求区间和,我们预处理出前缀和,用O(1)复杂度求出区间和
定义:dp[i]表示前i个人的最大收益,sum[i]表示前缀和
现在选的这一段区间和用sum[i]-sum[j]表示(i-k<=j<=i)
这段区间与 前一段长为k的区间 中间要空一个数(不能连续选k+1个数),于是我们固定i点,不断地枚举j点,找到和的最大值
因为sum求的是闭区间的前缀和 , sum[i]-sum[i]表示区间[ j+1 ,i ]的和,中间空的数就是j,再加上dp[j-1]就可以更新dp[i]的值
综上,我们可以得到状态转移方程
dp[i]=max( dp[j-1]+sum[i]-sum[j]) (i-k<=j<=i)
这里的max是对应每一个j点算出括号里的值,再求最大值
那么又怎么算最大值呢?总不能真的枚举j再找最大值吧?肯定不行,时间复杂度太高
但是你会突然发现
原方程可以拆分成 dp[i]=max(dp[j-1]-sum[j])+sum[i]
然后我们可以发现这个式子的变化与sum[i]无关,因为我们固定了i点,只是j点在变
那么我们考虑一个超棒的家伙——单调队列优化
(注意一下单调队列里面存的是下标)
定义f[j]=dp[j-1]-sum[j],将f[j]丢进单调队列里面,每次取出队首元素
扩展下一个点的时候,更新一下:f[i+1]=dp[i]-sum[i+1](和上面的式子是一样的),将它丢进单调队列里面就好啦
ヾ(๑╹◡╹)ノ"
>>>>代码
#include<bits/stdc++.h> #define maxn 2000005 #define ll long long using namespace std; int head=0,tail=1,n,k; ll dp[maxn],f[maxn],sum[maxn]; int a[maxn],q[maxn]; int read() { int x=0 ; char c=getchar() ; while(c<'0'||c>'9') c=getchar() ; while(c>='0'&&c<='9') { x=x*10+c-'0', c=getchar() ; } return x; } int main() { // freopen("group.in","r",stdin); // freopen("group.out","w",stdout); scanf("%d%d",&n,&k); for(int i=1;i<=n;i++) { a[i]=read(); sum[i]=sum[i-1]+a[i]; } for(int i=1;i<=n;i++) { while(head<tail&&i-q[head]>k) head++;//如果队首的元素已经不在范围里,踢出队列 dp[i]=f[q[head]]+sum[i];//取出队首元素,更新dp[i]的值 f[i+1]=dp[i]-sum[i+1];//更新 f[i+1] while(head<tail&&f[q[tail-1]]<f[i+1]) tail--;//维护单调队列,将小于f[i+1]的数都踢出去 q[tail++]=i+1; } printf("%I64d",dp[n]); return 0; }
>>>>总结
通过这道题目,我们还可以知道
对于一类dp,状态转移方程抽象为:dp[i]=max(f[j])+g[i] (l[i]<=j<=r[i])
并且l[i]~r[i]单调不减时,我们都可以考虑用单调队列优化
步骤:踢出过时元素,更新dp值,新元素入队并且维护单调队列
那么就到这里啦!
完结撒花٩(๑❛ᴗ❛๑)۶
题目来源: 2019.2.19杨雅儒学长的考试题