【总结】单调队列优化DP
待填的坑越来越多惹 QwQ
双端队列
前置芝士
队列(queue
)
正文
众所周知,STL有个东西叫 deque,但是为了装B,我们可以手写。
那么,双端队列(deque
)是个什么东西?和queue
有什么不同?
queue
是队尾(back
)进,队头(front
)出。
而deque
呢,是队尾进,队头、队尾都可以出。
所以可以比较轻易地得到代码。
template<class T>
class deque{
private:
T q[maxn];
int l,r;
public:
deque(void){
r=0;
l=1;
//l的初值必须比r大1,否则初始时的size就会为1
}
void clear(void){
r=0;
l=1;
//清空
}
T front(void){
return q[l];
//返回队头元素
}
T back(void){
return q[r];
//返回队尾元素
}
void push(T x){
q[++r]=x;
//向队尾插入元素
}
int size(void){
return r-l+1;
}
bool empty(void){
return !(r-l+1);
}
void pop_front(void){
if(l<=r)++l;
return;
//弹出队头
}
void pop_back(void){
if(l<=r)--r;
return;
//弹出队尾
}
};
//提示:在时限较小时还是使用STL中的吧(亲身经历
单调队列
前置芝士
往上翻。
正文
字面意思,单调队列=元素具有单调性的队列。
做 LIS 时,我们学过 一种贪心+二分的做法。
我们使一个数组内的元素保持从小到大,如果要插入的元素比数组末尾元素更大,直接插入;否则在数组内找到相应的位置替换原有的,贡献较小的值。
这个操作和单调队列有一点点像。但这个数组维护的是LIS的长度,单调队列维护的是区间最值。
具体怎么操作呢?
例题一:求m区间内的最小值
Solution
虽然这道题可以用滚动的ST表或动态开点的线段树水过,但我们还是假装它是一道单调队列的模板题。
我们思考,对于1~n
内的每一个 i
,如何才能去除多余元素,维护 a[i-m-1]~a[i-1]
的最小值?
我们使用一个双端队列q
,储存的元素均是 a
的下标。
我们假设此时q
内的下标在i
之前,且对应的元素已经从小到大有序。(即 a[q[l]]<a[q[l+1]]<a[q[l+2]]<...<a[q[r]]
)
最后取得的最大值就是 a[q[l]]
,也就是 a[q.first()]
。
首先我们要保证 q.first()
在 i-m-1~i-1
范围内,也就是 q.first()>=i-m-1
。
如果 q.first()<i-m-1
,说明这个值已经在范围之外了,大可直接 pop
掉。
因为可能下一个最小值同样不在这个范围内,我们重复判断,直到下一个下标在范围内或队列为空为止。
while(i-q.front()+1>m&&q.size())
q.pop_front();
接下来要做的就是将 i
插入且保持 q
的单调性。
若 a[i]
比 a[q.back()]
小,q.back()
一定是个废品,可以pop
掉。
重复“扔掉废品”的动作,直到下一个元素不是废品或队列为空为止。
Code
#include<cstdio>
const int maxn=2e6+5;
int n,m;
int a[maxn];
template<class T>
class deque{
private:
int q[maxn];
int l,r;
public:
deque(void){
r=0;
l=1;
}
void clear(void){
r=0;
l=1;
}
T front(void){
return q[l];
}
T back(void){
return q[r];
}
void push(T x){
q[++r]=x;
}
int size(void){
return r-l+1;
}
bool empty(void){
return !(r-l+1);
}
void pop_front(void){
if(l<=r)++l;
return;
}
void pop_back(void){
if(l<=r)--r;
return;
}
};
deque<int>q;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
printf("%d\n",a[q.front()]);
if(i-q.front()+1>m)
q.pop_front();
//因为每次都会删除范围外的,导致范围外的数最多只有连续的1个,故将while替换为if
while(a[i]<=a[q.back()]&&q.size())
q.pop_back();
q.push(i);
/*
最后插入元素,
为了防止下一个数无front可查(即除了当前数其他所有数都因为在下一个数范围外而删除)
必须插入当前数i
*/
}
return 0;
}
练习一:滑动窗口
Solution
和上一道题很像,只不过要同时维护最大值与最小值,以及一些语句的顺序问题。
Code
#include<cstdio>
const int maxn=1e6+5;
template<class T>
class deque{/*略*/};
int n,m;
deque<int>q1,q2;
int a[maxn],ans[maxn][2];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
while(a[i]<a[q1.back()]&&q1.size())
q1.pop_back();
while(a[i]>a[q2.back()]&&q2.size())
q2.pop_back();
q1.push(i);
q2.push(i);
if(i-q1.front()>=m)
q1.pop_front();
if(i-q2.front()>=m)
q2.pop_front();
ans[i][0]=a[q1.front()];
ans[i][1]=a[q2.front()];
//注意这里的顺序!
//可以自行尝试在草稿纸上模拟一下单调队列的过程
}
for(int i=m;i<=n;++i)
printf("%d ",ans[i][0]);
puts("");
for(int i=m;i<=n;++i)
printf("%d ",ans[i][1]);
return 0;
}
例题二:最大连续和
Solution
水题。
注意到 连续 两字,考虑使用前缀和优化。
用 sum
数组表示 A
数组的前缀和,对于每一个 \(i(1\leqslant i\leqslant n)\),找到 \(\min\{sum_j\}(i-m\leqslant j<i)\),那么 \(j\sim i\) 就是以 \(i\) 结尾的和最大的连续子序列。
是不是有点 DP 味儿了?
Tips:因为 \(1\sim i\) 也是一段连续的子序列,所以 \(sum_i-sum_0\) 也可能是答案,单调队列中应先
push(0)
Code
#include<cstdio>
const int maxn=2e5+5;
const int inf=0x3f3f3f3f;
template<class T>
class deque{/*略*/};
deque<int>q;
int n,m,ans=-inf;
int a[maxn],sum[maxn];
int max(int x,int y){return x>y?x:y;}
int main(){
scanf("%d%d",&n,&m);
q.push(0);
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
sum[i]=sum[i-1]+a[i];
if(i-q.front()>m)
q.pop_front();
ans=max(ans,sum[i]-sum[q.front()]);
while(sum[i]<=sum[q.back()]&&q.size())
q.pop_back();
q.push(i);
}
printf("%d",ans);
return 0;
}
例题三:旅行问题
Solution
看来出题人不想要他(她?)的 horse 了。
重点在于 顺时针(或逆时针) ,这代表着我们必须正着跑一遍,再反着跑一遍。
此题需要极强的耐力,调起来让人真心问候出题人的母亲,甚至调到最后你都不知道自己在写什么代码逻辑却很清晰明了,主要是细节问题。
到了这时,我们的算法越来越靠近 DP 了。所以我们想到 DP 处理环形问题的方法:将环变形成一条链。
对于顺时针的走法:
在每一站,可以获得的“真实油量”就是 \(p_i-d_i\)。
从第 \(i\) 站到第 \(j\) 站的最后油量(不管中途油够不够用)就是 \(\sum\limits_{k=i+1}^{j}p_k-d_k\)。
考虑对 \(p_i-d_i\) 的前缀和优化。
我们从后往前遍历每一站 \(i\),用单调队列储存在 \(i\sim i+n\) 范围内前缀和最小的站,若这个站的前缀和 \(-sum_i\) 的值为非负数,即以 \(i\) 为起点,到这个站时油够用。由于这个站是以 \(i\) 站为起点,全程剩余油量最小的一个站,既然这个站油够用,整个行程的有就够用。
逆时针同理。
Code
<法一>(乱写水过,作者本人都看不懂,可以跳过)
#include<cstdio>
#define int long long
const int maxn=2e6+5;
template<class T>
class deque{/*略*/};
int n,nn;
deque<int>q,q2;
bool vis[maxn];
int p[maxn],d[maxn];
int a[maxn],a2[maxn],sum[maxn],sum2[maxn];
signed main(){
scanf("%lld",&n);
for(int i=1;i<=n;++i){
scanf("%lld%lld",&p[i],&d[i]);
p[i+n]=p[i],d[i+n]=d[i];
a[i]=p[i]-d[i];
a[i+n]=p[i+n]-d[i+n];
sum[i]=sum[i-1]+a[i];
while(sum[i]<sum[q.back()]&&q.size())
q.pop_back();
q.push(i);
}
d[0]=d[n];
for(int i=1;i<=n;++i){
a2[i]=p[i]-d[i-1];
a2[i+n]=p[i+n]-d[i+n-1];
sum2[i]=sum2[i-1]+a2[i];
}
nn=n<<1;
for(int i=n+1;i<=nn;++i)
sum[i]=sum[i-1]+a[i];
for(int i=nn;i>n;--i){
sum2[i]=sum2[i+1]+a2[i];
while(sum2[i]<sum2[q2.back()]&&q2.size())
q2.pop_back();
q2.push(i);
}
for(int i=n;i;--i)
sum2[i]=sum2[i+1]+a2[i];
for(int i=1;i<=n;++i){
if(q.front()<i)
q.pop_front();
if(sum[q.front()]-sum[i-1]>=0)
vis[i]=1;
while(sum[i+n]<sum[q.back()]&&q.size())
q.pop_back();
q.push(i+n);
}
for(int i=nn;i>n;--i){
if(q2.front()>i)
q2.pop_front();
if(sum2[q2.front()]-sum2[i+1]>=0)
vis[i-n]|=1;
while(sum2[i-n+1]<sum2[q2.back()]&&q2.size())
q2.pop_back();
q2.push(i-n);
}
for(int i=1;i<=n;++i)
puts(vis[i]?"TAK":"NIE");
return 0;
}
<法二>(正解)
#include<cstdio>
#define int long long
const int maxn=2e6+5;
template<class T>
class deque{/*略*/};
int n,nn;
deque<int>q;
bool vis[maxn];
int p[maxn],d[maxn],a[maxn];
signed main(){
scanf("%lld",&n);
for(int i=1;i<=n;++i){
scanf("%lld%lld",&p[i],&d[i]);
p[n+i]=p[i],d[n+i]=d[i];
}
nn=n<<1;
//顺时针
for(int i=1;i<=nn;++i)
a[i]=a[i-1]+p[i]-d[i];
for(int i=nn;i;--i){
if(q.front()>=i+n)
q.pop_front();
while(a[i]<=a[q.back()]&&q.size())
q.pop_back();
q.push(i);
if(a[i-1]<=a[q.front()]&&i<=n)
vis[i]=1;
}
//逆时针
d[0]=d[n];
q.clear();
for(int i=1;i<=nn;++i)
a[i]=a[i-1]+p[i]-d[i-1];
for(int i=1;i<=nn;++i){
if(q.front()<=i-n)
q.pop_front();
if(i>n&&a[i]>=a[q.front()])
vis[i-n]=1;
while(a[q.back()]<=a[i]&&q.size())
q.pop_back();
q.push(i);
}
for(int i=1;i<=n;++i)
puts(vis[i]?"TAK":"NIE");
return 0;
}
例题四:修剪草坪
Solution
进入正题
看上去是一道 DP,我们先写出状态转移方程。
设 \(f_{i,0}\) 为不选第 \(i\) 头牛时的最大效率值,\(f_{i,1}\) 为选择第 \(i\) 头牛时的最大效率值,\(sum_i=\sum\limits_{j=1}^{i}E_i\),则有:
\(f_{i,0}\) 很好求,关键在于 \(f_{i,1}\) 的 \(\max\) ,如果不进行优化,算法将会有 \(\Theta(n^2)\) 的时间复杂度,明显爆掉。
既然是求极值,我们为何不用单调队列优化呢?
使用单调队列维护 \(\max\{f_{j,0}-sum_j\}\)。
Code
#include<cstdio>
#define int long long
const int maxn=1e5+5;
template<class T>
class deque{/*略*/};
int n,k;
deque<int>q;
int f[maxn][2];
int a[maxn],sum[maxn];
int max(int x,int y){return x>y?x:y;}
signed main(){
scanf("%lld%lld",&n,&k);
q.push(0);
for(int i=1;i<=n;++i){
scanf("%lld",&a[i]);
sum[i]=sum[i-1]+a[i];
f[i][0]=max(f[i-1][0],f[i-1][1]);
if(i-q.front()>k)
q.pop_front();
f[i][1]=f[q.front()][0]-sum[q.front()]+sum[i];
while(f[i][0]-sum[i]>f[q.back()][0]-sum[q.back()]&&q.size())
q.pop_back();
q.push(i);
}
printf("%lld",max(f[n][0],f[n][1]));
return 0;
}
练习四:绿色通道
设 \(f_i\) 表示抄第 \(i\) 题时最小用时。
二分最小空题段长度,水题。
#include<cstdio>
const int maxn=5e4+5;
template<class T>
class deque{/*略*/};
deque<int>q;
int a[maxn],f[maxn];
int n,t,ans,l,r,mid;
int max(int x,int y){return x>y?x:y;}
bool check(int x){
/*
状态
f[i]表示抄第i道题所花费的最小时间
DP柿子
f[i]=min{f[j]}+a[i] (i-x<=j<i)
*/
q.clear();
q.push(0);
for(int i=1;i<=n;++i){
if(q.front()<i-x-1)
q.pop_front();
f[i]=f[q.front()]+a[i];
while(f[i]<f[q.back()]&&q.size())
q.pop_back();
q.push(i);
}
return f[q.front()]<=t;
}
int main(){
scanf("%d%d",&n,&t);
for(int i=1;i<=n;++i)
scanf("%d",&a[i]);
l=1,r=n;
while(l<r){
mid=l+r>>1;
if(check(mid)){
ans=mid;
r=mid;
}
else l=mid+1;
}
printf("%d",ans);
return 0;
}
总结
总的方法就是:
- 推出 DP 柿子
- 将与 \(i\) 有关的项提到 \(\max/\min\) 的外面
- 单调队列维护 \(\max/\min\) 内的值
也就是说,满足以上条件的DP,可以使用单调队列优化。
那么,如果有类似 \(f_i=\min\{f_j+a_i\times a_j\}\) 这样无法直接将与 \(i\) 有关的项提出来的话,怎么办呢?
我们将会在斜率优化DP中讲到。
拓展难题
Dragon Ball
提示
绝对值处理起来很麻烦,为什么不尝试将每个周期的地图排序后,先处理当前点左边的点,再处理右边的呢?
Code
#include<cstdio>
#include<algorithm>
using std::sort;
const int inf=0x3f3f3f3f;
template<class T>
class deque{/*略*/};
struct ball{
int val,pos;
bool operator<(const ball q)const{
return pos<q.pos;
}
}a[55][1005];
deque<int>q;
int f[55][1005];
int T,n,m,x,ans,i,j,k;//不卡常过不了
int min(int x,int y){return x<y?x:y;}
int abs(int x){return x>=0?x:-x;}
int main(){
scanf("%d",&T);
while(T--){
ans=inf;
scanf("%d%d%d",&m,&n,&x);
for(i=1;i<=m;++i){
for(j=1;j<=n;++j)
scanf("%d",&a[i][j].pos);
}
for(i=1;i<=m;++i){
for(j=1;j<=n;++j)
scanf("%d",&a[i][j].val);
sort(a[i]+1,a[i]+n+1);
}
for(i=1;i<=n;++i)
f[1][i]=abs(a[1][i].pos-x)+a[1][i].val;
for(i=2;i<=m;++i){//枚举周期
q.clear();
for(j=k=1;j<=n;++j){//枚举当前节点为j
f[i][j]=inf;
while(k<=n&&a[i-1][k].pos<=a[i][j].pos){
int tmp=f[i-1][k]-a[i-1][k].pos;
while(q.size()&&tmp<f[i-1][q.back()]-a[i-1][q.back()].pos)
q.pop_back();
q.push(k++);
}
if(q.size())
f[i][j]=f[i-1][q.front()]+a[i][j].pos-a[i-1][q.front()].pos+a[i][j].val;
}
q.clear();
for(j=k=n;j;--j){
while(k&&a[i-1][k].pos>=a[i][j].pos){
int tmp=f[i-1][k]+a[i-1][k].pos;
while(q.size()&&tmp<f[i-1][q.back()]+a[i-1][q.back()].pos)
q.pop_back();
q.push(k--);
}
if(q.size())
f[i][j]=min(f[i][j],f[i-1][q.front()]-a[i][j].pos+a[i-1][q.front()].pos+a[i][j].val);
}
}
for(i=1;i<=n;++i)
ans=min(ans,f[m][i]);
printf("%d\n",ans);
}
return 0;
}
Wilcze doły
提示
用单调队列维护 \(\max\{w_{i-d}\sim w_i\}\) 。需要思考明白以 \(i\) 结尾的序列的左端点不可能比以 \(i-1\) 结尾的序列的左端点更靠左这件事。
Code
#include<cstdio>
#define int long long
const int maxn=2e6+5;
template<class T>
class deque{/*略*/};
deque<int>q;
int n,p,d,lst,ans;
int w[maxn],s[maxn];
void read(int&x){
x=0;
bool f=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')f=1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+(ch^48);
ch=getchar();
}
if(f)x=-x;
return;
}
int max(int x,int y){return x>y?x:y;}
signed main(){
read(n);read(p);read(d);
for(int i=1;i<=n;++i){
read(w[i]);
w[i]+=w[i-1];
}
for(int i=d;i<=n;++i)
s[i]=w[i]-w[i-d];
ans=d;
lst=1;//区间起点,具有单调性
//使用单调队列维护 [i-d,i] 的和的最大值
q.push(d);
for(int i=d+1;i<=n;++i){
while(s[i]>s[q.back()]&&q.size())
q.pop_back();
q.push(i);
while(q.front()-d+1<lst&&q.size())
q.pop_front();
//sum[lst,i]-sum[j-d,j]如果大于p
//也就是说这一段不符合条件,弹出。
while(w[i]-w[lst-1]-s[q.front()]>p&&q.size()){
lst++;
while(q.front()-d+1<lst&&q.size())
q.pop_front();
}
ans=max(ans,i-lst+1);
}
printf("%lld",ans);
return 0;
}
end.
—— · EOF · ——
真的什么也不剩啦 😖