单调队列优化 dp 浅谈与刷题笔记
单调队列优化 dp 浅谈
1 形式
在 1D1D dp 优化中,单调队列是最基础的一中,通常是对决策集合的单调维护。
如果一个 dp 方程满足这样的形式 \(f_{j}=\max or\min\{f_{k}+val_{j,k}\}\) 其中 \(val_{j,k}\) 只与 \(j\) 或 \(k\) 有关,那么就可以用单调队列优化。
我们直接实战演练:
2 例题
2.1 P1776 宝物筛选
单调队列优化多重背包。
多重背包的朴素做法是这样的:
其中 \(cnt\) 为物品个数,\(w\) 为物品重量,\(c\) 为物品价值。
我们只保留决策变量和状态变量,(不保留阶段变量,即不保留 \(i\) )有这样的转移式子:
我们令 \(j=a\times w_i+b,(0\le b\le w_i-1)\) 可以得到:
然后令 \(k=a-y\)
至于为什么 \(y\) 要大于 \(0\) 是因为要保证 \(b+y\times w_i\) 非负。
我们看到这个式子是一个标准的可以用单调队列优化的形式。
所以我们先枚举阶段,再枚举余数 \(b\) ,然后枚举 \(a\) ,单调队列优化转移就可以,复杂度 \(O(n\times V)\) 。
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 100100
#define M 101
using namespace std;
const int INF=0x3f3f3f3f;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
int n,W,v[N],w[N],m[N],f[M][N];
int head,tail,q[N];
inline int compeat(int k,int yu,int id){
return f[id-1][yu+k*w[id]]-k*v[id];
}
int main(){
read(n);read(W);
for(int i=1;i<=n;i++){
read(v[i]);read(w[i]);read(m[i]);
}
for(int i=1;i<=n;i++){
for(int j=0;j<w[i];j++){
head=tail=0;q[++tail]=0;
for(int k=0;k<=(W-j)/w[i];k++){
while(head<tail&&q[head+1]<k-m[i]) head++;
if(head<tail){
int now=q[head+1];
f[i][k*w[i]+j]=f[i-1][now*w[i]+j]-now*v[i]+k*v[i];
}
while(head<tail&&compeat(q[tail],j,i)<compeat(k+1,j,i)) tail--;
q[++tail]=k+1;
}
}
}
printf("%d",f[n][W]);
return 0;
}
2.2 P3089 [USACO13NOV]Pogo-Cow S
我们先想固定方向怎么做,另一个方向是对称的。
一个自然的状态是 \(f_{i,j}\) 表示上一次从一个距离为 \(j\) 的地方跳过来的最大值,但是第二维有 \(10^6\) ,怎么办?不难想到我们可以用位置来代替距离。即 \(f_{i,j}\) 表示目前在 \(i\) ,然后上一次是从 \(j\) 跳过来的最大值是多少。
怎么转移,不难想到:
这样的话转移是 \(n^3\) 的,怎么办?
我们固定 \(j\) ,不难发现随着 \(i\) 的变化,\(k\) 的取值范围是逐渐向左扩大的,且只增不减。
于是我们可以用决策的单调性把它优化成三维的。
在实现中注意 \(nowmax\) 的初值,表示可以从这里开始。
至于另一个方向,把数组翻转后变一下号就可以了。
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 1010
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
inline int Max(int a,int b){
return a>b?a:b;
}
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
struct node{
int x,p;
bool operator < (const node &b){
return x<b.x;
}
};
node a[N];
int f[N][N],n,q[N],l,r;
int maxx=-INF;
int main(){
read(n);
for(int i=1;i<=n;i++){
read(a[i].x);read(a[i].p);
}
sort(a+1,a+n+1);
for(int j=1;j<=n;j++){
int nowmax=a[j].p,k=j;
for(int i=j+1;i<=n;i++){
while(k>0&&a[j].x-a[k-1].x<=a[i].x-a[j].x){
k--;
nowmax=Max(nowmax,f[j][k]);
}
f[i][j]=nowmax+a[i].p;
maxx=Max(maxx,f[i][j]);
}
}
reverse(a+1,a+n+1);
memset(f,0,sizeof(f));
for(int j=1;j<=n;j++){
int nowmax=a[j].p,k=j;
for(int i=j+1;i<=n;i++){
while(k>0&&a[j].x-a[k-1].x>=a[i].x-a[j].x){
k--;
nowmax=Max(nowmax,f[j][k]);
}
f[i][j]=nowmax+a[i].p;
maxx=Max(maxx,f[i][j]);
}
}
printf("%d",maxx);
return 0;
}
2.3 P3572 [POI2014]PTA-Little Bird
有些时候一些可以用单调队列优化的题并不是那么显然,需要分析单调性,比如说这到。
转移方程不难写出:
不难发现这是一个滑动窗口,但是权值如何处理?这个权值即与 \(i\) 有关,又与 \(j\) 有关。
那么我们可以这样做,这个权值最多就是 \(1\) ,在单调队列中,我们让 \(f\) 值小的有限,我们去掉那些高度低且 \(f\) 值大的点。
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 1000100
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
int f[N],q[N],l,r,m;
int n,d[N];
inline bool compare(int k,int i){
if(f[k]!=f[i]) return f[k]>f[i];
return d[k]<=d[i];
}
int main(){
read(n);
for(int i=1;i<=n;i++) read(d[i]);
read(m);
for(int i=1;i<=m;i++){
int k;read(k);l=r=0;q[++r]=1;
for(int i=2;i<=n;i++){
while(l<r&&q[l+1]<i-k) l++;
if(l<r){
int now=q[l+1];
f[i]=f[now]+(d[now]<=d[i]);
}
while(l<r&&compare(q[r],i)) r--;
q[++r]=i;
}
printf("%d\n",f[n]);
for(int i=2;i<=n;i++) f[i]=0;
}
return 0;
}
2.4 P3522 [POI2011]TEM-Temperature
比较恶心的一道题,属于单调队列的直接应用,一道思维题。
不难发现,我们需要知道每一天前面的最低温度最大值是多少以判断是否能够使温度不将,所以我们开一个单调队列,保证从队头到队尾的最低温度是越来越小,这样,当我们到第 \(i\) 天的时候,我们让最低温度大于其最高温度的队头弹出去,然后这里的天数是用当前天数减去队头的前一天。
代码比较好写。
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 1001000
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
inline int Max(int a,int b){
return a>b?a:b;
}
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
int f[N],q[N],l,r,ans=-INF,n;
struct node{
int minn,maxx;
};
node a[N];
int main(){
read(n);
for(int i=1;i<=n;i++){
read(a[i].minn);read(a[i].maxx);
}
for(int i=1;i<=n;i++){
while(l<r&&a[q[l+1]].minn>a[i].maxx) l++;
ans=Max(ans,i-q[l]);
while(l<r&&a[q[r]].minn<a[i].minn) r--;
q[++r]=i;
}
printf("%d",ans);
return 0;
}
2.5 P4544 [USACO10NOV]Buying Feed G
题目不难,但是因为今天心情不好,所以失误比较多,调了较长时间。
做这个题以前没有注意到的一点是:虽然结果没有溢出,但是中间过程中可能有溢出的情况,会导致答案不对,所以我们应该加一句话:if(g[i][j]<0) g[i][j]=INF
以防止这种情况发生。
这是一个裸的单调队列优化 dp,对的起其难度,dp 方程不难想到:
其中 \(g_{i,j}\) 表示考虑完前 \(i\) 个点,目前手里有 \(j\) 个肥料的最小花费。
不难发现这个可以用单调队列优化。
写得时候错在了以下几点:
- 计算单调队列队尾弹出时符号打反,计算值错了。
- 进入决策集合的变量弄错。
代码:
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define int long long
#define uint unsigned int
#define ull unsigned long long
#define N 600
#define M 20010
using namespace std;
const int INF=0x3f3f3f3f;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
template<typename T> inline T Min(T a,T b){
return a>b?b:a;
}
template<typename T> inline T Max(T a,T b){
return a>b?a:b;
}
struct rode{
int x,c,f;
inline bool operator < (const rode &b) const{
return x<b.x;
}
};
rode a[N];
int k,n,e;
int g[N][M],q[M<<3],l,r;
inline int compeat(int id,int k){
return g[id-1][k]+(a[id].x-a[id-1].x)*k*k-k*a[id].c;
}
signed main(){
read(k);read(e);read(n);
for(int i=1;i<=n;i++){
read(a[i].x);read(a[i].f);read(a[i].c);
}
sort(a+1,a+n+1);
memset(g,INF,sizeof(g));
for(int j=0;j<=Min(k,a[1].f);j++) g[1][j]=j*a[j].c;
for(int i=1;i<=n;i++) g[i][0]=0;
for(int i=2;i<=n;i++){
l=r=0;q[++r]=0;
for(int j=0;j<=k;j++){
while(l<r&&q[l+1]<Max((ll)0,j-a[i].f)) l++;
if(l<r){
int now=q[l+1];
g[i][j]=g[i-1][now]+(a[i].x-a[i-1].x)*now*now-now*a[i].c+j*a[i].c;
}
if(g[i][j]<0) g[i][j]=INF;
while(l<r&&compeat(i,q[r])>compeat(i,j+1)) r--;
q[++r]=j+1;
}
}
printf("%lld",g[n][k]+(k*k)*(e-a[n].x));
return 0;
}
2.6 P4852 yyf hates choukapai
这道题调了很久,也带给我一些反思,反思写到最后。
2.6.1 状态设计
我的状态设计与大部分题解的并不相同,我的状态数会更少一些,设 \(f_{i,j}\) 表示一共抽了 \(j\) 次,其中,有 \(i\) 次是连抽,并且第 \(j\) 次抽是连抽。
2.6.2 转移
首先,我们在序列的后面加上 \(c\) 个 \(0\) ,这样我们就可以强制最后一次是连抽而不影响正确性。
我们枚举前一次连抽是第几次:
其中 \(sum\) 是前缀和,\(now\) 是状态 \(f_{i,j}\) 对应的位置,也就是 \(i\times c+(j-i)\) ,\(last\) 是 \(f_{i-1,k}\) 对应的位置,计算方法和左边相同。那么这个就是一个裸的单调队列。
记录方案的话就开一个数组对应记一下就可以了。
2.6.3 注意事项
- 在 dp 中,所有的变量的范围一定要卡死。
- 所有不合法的状态一定不要随意赋值。
- 不要随意的初始化。
- 所做的一切操作一定要符合你的状态。
不遵守上述事项的结果就是我调了一天。
2.6.4 代码:
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
// #define int long long
#define uint unsigned int
#define ull unsigned long long
#define N 310000
#define M 70
using namespace std;
const int INF=0x3f3f3f3f;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
int n,m,c,d,a[N],f[M][100000],sum[N],g[M][100000];
int q[N],l,r;
inline int get_posi(int id,int jd){
return id*c+(jd-id);
}
inline void prework(){
n++;
for(int i=1;i<=n*c+m;i++) sum[i]=a[i]+sum[i-1];
}
inline int compeat(int id,int k){
return f[id-1][k]-sum[get_posi(id-1,k)];
}
inline void print(int id,int jd){
if(g[id][jd]<=0) return;
print(id-1,g[id][jd]);
int posi=get_posi(id-1,g[id][jd])-c+1;
if(posi>0) printf("%d ",posi);
}
int main(){
read(n);read(m);read(c);read(d);
for(int i=1;i<=n*c+m;i++) read(a[i]);
prework();
for(int i=1;i<=n;i++){
l=r=0;
for(int j=0;j<=n+m&&j<=(d+1)*i;j++){
while(l<r&&(q[l+1]<j-d-1||q[l+1]<i-1)) l++;
if(j>=i&&l<r){
int k=q[l+1];
int now=get_posi(i,j),last=get_posi(i-1,k);
f[i][j]=f[i-1][k]-sum[last]+sum[now-c+1];
g[i][j]=k;
}
while(l<r&&compeat(i,q[r])<compeat(i,j)) r--;
q[++r]=j;
}
}
printf("%d\n",f[n][n+m]);
print(n,n+m);
return 0;
}