概念
单调队列优化 d p dp dp,就是指用单调队列来优化的 d p dp dp。
方法
一般是针对于求最值的问题。在队列中加入某一个数,如果有些方案显然在后面是不会用到的则直接弹出即可。一般处理再 d p dp dp中有要求一段区间的最小值或最大值等等。还有一种是滑动窗口,指在一个序列的一段区间内求最值的问题,就像是一个窗口框柱一些框内的数,然后在框内找最值。
例题
AcWing 135
因为要求的是某一段区间的和,所以可以用前缀和来得到一段区间的和。首先,我们发现,求一段区间的和 s r − s l − 1 s_r-s_{l-1} sr−sl−1,枚举到 r r r肯定 s r s_r sr是一定的,那么要区间和尽量大就是让 s l − 1 s_{l-1} sl−1尽量小,则加入一个数 s i s_i si作为 s l − 1 s_{l-1} sl−1时前面比 s i s_i si大的都不可能用到了,就是个单调递增队列。每次取队头作为 s l − 1 s_{l-1} sl−1就是以 s i s_i si为 s r s_r sr的区间的最优方案。注意,如果区间长度大于 m m m了要弹出一个数。注意,如果最大的是从头开始就是 − s 0 -s_0 −s0,所以最开始要压入一个 0 0 0(这里直接令 h = t = 0 h=t=0 h=t=0即可)。
#include<bits/stdc++.h>
using namespace std;
const int NN=300004;
int s[NN],q[NN];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
s[i]=s[i-1]+x;
}
int ans=-1e9,h=0,t=0;
for(int i=1;i<=n;i++)
{
if(i-q[h]>m)
h++;
if(h<=t)
ans=max(ans,s[i]-s[q[h]]);
while(h<=t&&s[q[t]]>=s[i])
t--;
q[++t]=i;
}
printf("%d",ans);
return 0;
}
AcWing 1087
本题可以想到,设 f i f_i fi为前 i i i头牛合法选择的最大效益。首先可以不选, f i = f i − 1 f_i=f_{i-1} fi=fi−1。考虑选择第 i i i头牛,可以枚举一个 j j j表示 i i i开始往前连续的牛的数量,保证 1 ≤ j ≤ k 1\le j\le k 1≤j≤k即可。则这一段牛就是 ∑ x = i − j + 1 i \displaystyle\sum_{x=i-j+1}^i x=i−j+1∑i,为了保证不选第 i − j i-j i−j头牛,那么就是用 f i − j − 1 f_{i-j-1} fi−j−1。于是有状态转移方程 f i = max ( f i − 1 , max ( f i − j − 1 + ∑ x = i − j + 1 i , 1 ≤ j ≤ k ) ) f_i=\max(f_{i-1},\max(f_{i-j-1}+\displaystyle\sum_{x=i-j+1}^i,1\le j\le k)) fi=max(fi−1,max(fi−j−1+x=i−j+1∑i,1≤j≤k))。有了状态转移方程,发现有一个求区间和的过程,用前缀和。发现中间的 j j j求的是一个 max \max max,可以用单调队列维护。因为 f f f可以从 f 0 f_0 f0迭代,所以要先压入一个 0 0 0。
#include<bits/stdc++.h>
using namespace std;
const int NN=1e5+4;
long long s[NN],f[NN];
int q[NN];
int main()
{
int n,k;
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
{
scanf("%lld",&s[i]);
s[i]+=s[i-1];
}
int h=0,t=0;
for(int i=1;i<=n;i++)
{
if(q[h]<i-k)
h++;
if(h<=t)
f[i]=max(f[i-1],f[q[h]-1]+s[i]-s[q[h]]);
while(h<=t&&f[q[t]-1]-s[q[t]]<=f[i-1]-s[i])
t--;
q[++t]=i;
}
printf("%lld",f[n]);
return 0;
}
AcWing 1088
可以选择顺时针或者逆时针,那么做两次即可。因为是环,所以再接一段序列。记录从一条路径经过所需要或者能获得的油,如果这样不断累加有一段小于 0 0 0就不可行,因为要计算一段,所以用前缀和。我们要求每一段序列之和都小于 0 0 0,所以只要要求每一段序列之和都大于等于 0 0 0即可。因为计算某个区间的和是 s r − s l − 1 s_r-s_{l-1} sr−sl−1,所以找最大的一个 s l − 1 s_{l-1} sl−1看看能不能使 s r − s l − 1 s_r-s_{l-1} sr−sl−1小于 0 0 0即可。因为要找最大值,所以可以用优先级队列维护。因为本题是算的区间长度,可能减去 s 0 s_0 s0,所以要先压入一个 0 0 0。
#include<bits/stdc++.h>
using namespace std;
const int NN=1e6+4;
long long s[2*NN];
bool ans[NN];
int p[NN],d[NN],a[2*NN],q[2*NN];
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d%d",&p[i],&d[i]);
a[i]=a[i+n]=p[i]-d[i];
}
for(int i=1;i<=2*n;i++)
s[i]=s[i-1]+a[i];
int h=0,t=0;
for(int i=1;i<=2*n;i++)
{
if(q[h]<i-n+1)
h++;
while(s[q[t]]>=s[i]&&h<=t)
t--;
q[++t]=i;
if(h<=t&&i>=n&&s[q[h]]>=s[i-n])
ans[i-n+1]=true;
}
for(int i=1;i<n;i++)
a[i]=a[i+n]=p[n-i+1]-d[n-i];
a[n]=p[1]-d[n];
for(int i=1;i<=2*n;i++)
s[i]=s[i-1]+a[i];
t=h=0;
for(int i=1;i<=2*n;i++)
{
if(q[h]<i-n+1)
h++;
while(s[q[t]]>=s[i]&&h<=t)
t--;
q[++t]=i;
if(h<=t&&i>=n&&s[q[h]]>=s[i-n])
ans[2*n-i]=true;
}
for(int i=1;i<=n;i++)
if(ans[i])
puts("TAK");
else
puts("NIE");
return 0;
}
AcWing 1089
设 f i f_i fi为前 i i i个位置,且第 i i i个位置要放烽火台,最少放多少烽火台可行。因为只要中间没有隔 m m m个烽火台就是可行的,则 f i = min ( f j , i > j , i − j < = m ) + a i f_{i}=\min(f_{j},i>j,i-j<=m)+a_i fi=min(fj,i>j,i−j<=m)+ai。因为有一个求区间的最小值的过程,可以用单调队列维护。考虑计算答案,枚举满足要求的情况下,最后一个烽火台放在哪里,取这些方案的最小值即可。本题 f f f可能从 0 0 0迭代,要先压入一个 0 0 0。
#include<bits/stdc++.h>
using namespace std;
const int NN=2*1e5+4;
int a[NN],f[NN],q[NN];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
int h=0,t=0;
for(int i=1;i<=n;i++)
{
if(q[h]<i-m)
h++;
if(h<=t)
f[i]=f[q[h]]+a[i];
while(h<=t&&f[q[t]]>=f[i])
t--;
q[++t]=i;
}
int res=1e9;
for(int i=n-m+1;i<=n;i++)
res=min(res,f[i]);
printf("%d",res);
return 0;
}
AcWing 1090
本题和上一题非常像。考虑如何转换成那一题,发现本题中缺少的就是连续不选择的限制,这也刚好是本题要求的,那么就二分答案。然后有了这个限制,按那一题的做法计算最少用的代价,看看能不能有一种方法在 t t t的时间内完成,这就是二分答案的 c h e c k check check函数。因为 f f f可以从 f 0 f_0 f0迭代,所以要先压入一个 0 0 0。
#include<bits/stdc++.h>
using namespace std;
const int NN=5*1e4+4;
int n,t,a[NN],f[NN],q[NN];
bool check(int mid)
{
int head=0,tail=0;
for(int i=1;i<=n;i++)
{
if(q[head]+mid+1<i)
head++;
if(head<=tail)
f[i]=f[q[head]]+a[i];
while(f[q[tail]]>=f[i]&&head<=tail)
tail--;
q[++tail]=i;
}
for(int i=n-mid;i<=n;i++)
if(f[i]<=t)
return true;
return false;
}
int main()
{
scanf("%d%d",&n,&t);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
int l=1,r=n;
while(l<r)
{
int mid=l+(r-l)/2;
if(check(mid))
r=mid;
else
l=mid+1;
}
printf("%d",r);
return 0;
}
AcWing 1091
本题是一个二维的滑动窗口。可以先竖着求一遍在 n n n行内的最小值和最大值,这样就相当于把 n n n行压缩成一个数,这样就压缩了一维。然后再横着求一边一维的滑动窗口即可。
#include<bits/stdc++.h>
using namespace std;
const int NN=1004;
int g[NN][NN],maxn[NN][NN],minn[NN][NN],q1[NN],q2[NN];
int main()
{
int a,b,n,ans=1e9,h1,h2,t1,t2;
scanf("%d%d%d",&a,&b,&n);
for(int i=1;i<=a;i++)
for(int j=1;j<=b;j++)
scanf("%d",&g[i][j]);
for(int i=1;i<=a;i++)
{
h1=0;
h2=0;
t1=-1;
t2=-1;
for(int j=1;j<=b;j++)
{
if(h1<=t1&&q1[h1]<=j-n)
h1++;
if(h2<=t2&&q2[h2]<=j-n)
h2++;
while(h1<=t1&&g[i][q1[t1]]<=g[i][j])
t1--;
while(h2<=t2&&g[i][q2[t2]]>=g[i][j])
t2--;
q1[++t1]=q2[++t2]=j;
if(h1<=t1&&j>=n)
maxn[i][j-n+1]=g[i][q1[h1]];
if(h2<=t2&&j>=n)
minn[i][j-n+1]=g[i][q2[h2]];
}
}
for(int i=1;i<=b-n+1;i++)
{
h1=0;
h2=0;
t1=-1;
t2=-1;
for(int j=1;j<=a;j++)
{
if(h1<=t1&&q1[h1]<=j-n)
h1++;
if(h2<=t2&&q2[h2]<=j-n)
h2++;
while(h1<=t1&&maxn[q1[t1]][i]<=maxn[j][i])
t1--;
while(h2<=t2&&minn[q2[t2]][i]>=minn[j][i])
t2--;
q1[++t1]=q2[++t2]=j;
if(h1<=t1&&h2<=t2&&j>=n)
ans=min(ans,maxn[q1[h1]][i]-minn[q2[h2]][i]);
}
}
printf("%d",ans);
return 0;
}