洛谷 P2034 选择数字 (单调队列优化dp)
题意
https://www.luogu.com.cn/problem/P2034
给出 \(n\) 个正整数 \(a_1 \sim a_n\) 。可以选一些数,但不能有超过 \(k\) 个连续的数字被选择。请求出选出的数的最大值。
题目分析
看到题面,第一眼是想到这种题:
给出 \(n\) 个正整数 \(a_1 \sim a_n\) 。可以选一些数,但不能有相邻的数字被选择。请求出选出的数的最大值。
这道题比较简单,只需dp记录每个数字拿或不拿的情况,给出代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e5+5;
int n,a[N];
ll f[N][2];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++){
f[i][0]=max(f[i-1][0],f[i-1][1]);
f[i][1]=f[i-1][0]+a[i];
}
printf("%lld",max(f[n][0],f[n][1]));
return 0;
}
但是原题要求连续 \(k\) 个数不能选,那么就需要修改状态转移方程。
之前是通过 \(i-1\) 个状态来推导第 \(i\) 个状态,但是当连续 \(k\) 个数不能选时,状态需要影响到前 \(k-1\) 个状态。
接下来开始推导,当这个数选时,那么前 \(k-1\) 个数不许有数不选,根据贪心,这个数不选后,后面全选时是最大和,那么可以推导出状态转移方程:
\(f_{i,0}=\max ~ f_{i,0},f_{i,1}\)
\(f_{i,1}=\max _{j=i-k+1}^{i-1}~ f_{j,0}+\sum_{l=j+1}^i a_l\)
区间和可以用前缀和维护,而长度相等区间最大值需要另一个数据结构维护————单调队列。
给出单调队列例题:https://www.luogu.com.cn/problem/P1886
\(l\) 从 \(1\) 到 \(n-l+1\) ,求 \(\max_{i=l}^{l+k-1} a_i\)
这里不展开描述,给出代码
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,k,a[N];
int q[N],l,r;
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
l=1,r=0;
for(int i=1;i<=n;i++){
while(l<=r&&a[i]>a[q[l]]) l++;
q[--l]=i;
if(i-q[r]==k) r--;
if(i>=k) printf("%d ",a[q[r]]);
}
return 0;
}
最后给出原题的代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e5+5;
int n,k,a[N];
int l,r,q[N];
ll s[N],f[N][2];
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i];
l=r=1;
for(int i=1;i<=n;i++){
f[i][0]=max(f[i-1][0],f[i-1][1]);
while(q[l]<i-k&&l<=r) l++;
f[i][1]=f[q[l]][0]-s[q[l]]+s[i];
while(f[i][0]-s[i]>f[q[r]][0]-s[q[r]]) r--;
r++;
q[r]=i;
}
printf("%lld",max(f[n][0],f[n][1]));
return 0;
}