众所周知,队列的两端是一端进,一端出。而双端队列(Double-ended queue, Deque),则是两端均可压入、弹出。压入操作分为 push_front()
和 push_back()
,弹出操作也分为 pop_front()
和 pop_back()
。
如果你学了队列的话会更好理解。没学过队列可以看「数据结构详解·四」队列学习队列。
我们主要讲讲双端队列的实现。
和队列类似,不再赘述。有时不建议用。
C++ STL 为我们提供了 deque
来实现双端队列。操作函数就是上面所说的,这里也不再赘述。
当然,STL deque 是可以和 STL vector 一样进行遍历的。
不过,STL deque 不管是时间还是空间,都具有巨大常数,在某些情况下不建议使用。
比如,Luogu B3656 【模板】双端队列 1。
我们可以很快地写出使用 deque 的代码。
然而,它 MLE 了。
因为它的巨大常数。
那该怎么办?
众所周知,这是实现的双向链表。但是仔细想想,如果我们不去遍历双端队列的值,是不是就相当于一个弱化的双向链表?正是因为链表无法直接访问值,所以我们可以用 STL list 代替 STL deque。
参考代码:
#include
using namespace std;
list<int>a[1000005];
int main()
{
ios::sync_with_stdio(0);
int q;
cin>>q;
while(q--)
{
string op;
int x,y;
cin>>op>>x;
if(op=="push_back")
{
cin>>y;
a[x].push_back(y);
}
else if(op=="pop_back"&&a[x].size()) a[x].pop_back();
else if(op=="push_front")
{
cin>>y;
a[x].push_front(y);
}
else if(op=="pop_front"&&a[x].size()) a[x].pop_front();
else if(op=="size") cout<<a[x].size()<<endl;
else if(op=="front"&&a[x].size()) cout<<a[x].front()<<endl;
else if(op=="back"&&a[x].size()) cout<<a[x].back()<<endl;
}
return 0;
}
“看起来好像双端队列并没有什么用处啊……”
你别急,看看标题。
是的,单调队列就是双端队列的最重要的应用。
单调队列(Monotone queue),顾名思义,就是数据具有单调性1的队列。
单调队列可以优化许多算法——本篇文章主要讲滑动区间最值的单调队列优化。
这是一道经典的单调队列。
如果我们考虑枚举每个区间,对于每个区间去挨个计算的话,时间复杂度约为 O ( n k ) O(nk) O(nk),无法通过题目。
这个时候,单调队列就派上用场了。
就样例为例:
8 3
1 3 -1 -3 5 3 6 7
先考虑最小值。
由于是最小值,我们可以考虑将队列的单调性变为单调递增的——这样,只要输出队列的第一个数据即可。
于是随着窗口的滑动,我们可以实时求出每次窗口滑动后的最小值。
我们考虑存入单调队列的是 i i i 而不是 a i a_i ai。因为在滑动窗口时,不在窗口内的数字要弹出,如果存入 a i a_i ai,就不知道这个数字需不需要弹出了。
同时,我们要注意,在窗口滑动时,如果新的数比队列中的一些数小,这就意味着这些数不再可能是最小值,我们需要把其从队尾弹出。
最大值同理,我们只要让队列是单调递减的即可。
参考代码:
#include
using namespace std;
int a[1000005];
deque<int>q;
int main()
{
int n,k;
cin>>n>>k;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++)
{
while(!q.empty()&&a[i]<a[q.back()]) q.pop_back();//有新的比其更小的数进来,那么所有在队列中比其大的数此后永远不可能是最小值,因而弹出(注意判断队列是否为空)
q.push_back(i);
while(q.front()<=i-k) q.pop_front();//弹出不在窗口内的
if(i>=k) cout<<a[q.front()]<<' ';//最前面的就是最小值
}
cout<<endl;
q.clear();
for(int i=1;i<=n;i++)
{
while(!q.empty()&&a[i]>a[q.back()]) q.pop_back();//有新的比其更大的数进来,那么所有在队列中比其小的数此后永远不可能是最大值,因而弹出(注意判断队列是否为空)
q.push_back(i);
while(q.front()<=i-k) q.pop_front();//弹出不在窗口内的
if(i>=k) cout<<a[q.front()]<<' ';//最前面的就是最大值
}
return 0;
}
可以发现,这段代码中,循环了 n n n 次,每个 a i a_i ai 最多进队列一次,出队列一次,因此时间复杂度变为 O ( n ) O(n) O(n)。
如果我们直接枚举 k k k,再模拟,时间复杂度就是 O ( n 2 ) O(n^2) O(n2),爆炸。
首先看到环形,容易想到破环为链2。
心情的变化是基于之前的进行累加变化,想一想,这像什么?没错,就是前缀和。
现在题目变成了:在一个长度为 2 n 2n 2n 的序列 A A A 中,有多少段长度为 n n n 的区间 [ k , k + n − 1 ] [k,k+n-1] [k,k+n−1] ,对于 ∀ i ∈ [ k , k + n − 1 ] \forall i\in[k,k+n-1] ∀i∈[k,k+n−1],满足 ∑ j = k i A j ≥ 0 \sum\limits_{j=k}^iA_j\ge0 j=k∑iAj≥0。
区间肯定是要枚举的,想想怎么满足后面的条件?
是不是只要满足区间内最小的 ∑ j = k i A j ≥ 0 \sum\limits_{j=k}^iA_j\ge0 j=k∑iAj≥0 即可?
想到了什么?
对!单调队列!
我们只要做一个区间前缀和最小值的滑动窗口即可!
参考代码:
#include
using namespace std;
deque<int>q;
int n,a[2000005],sum[2000005],ans;
signed main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i],a[i+n]=a[i];//破环为链
for(int i=1;i<=n*2-1;i++) sum[i]=sum[i-1]+a[i];//前缀和
for(int i=1;i<=n*2-1;i++)
{
while(!q.empty()&&max(i-n+1,1)>q.front()) q.pop_front();//窗口的滑动
while(!q.empty()&&sum[i]<=sum[q.back()]) q.pop_back();//注意是前缀和最小值
q.push_back(i);
if(i-n+1>0&&sum[q.front()]-sum[i-n]>=0) ans++;//如果最小的心情是非负数那么方案数加一
}
cout<<ans;
return 0;
}
也就是这串序列满足单调递(不)增或递(不)减,比如 { 114 , 514 , 1919810 } \{114,514,1919810\} {114,514,1919810} 是满足单调递增的序列, { 11451 , 4191 , 981 , 0 } \{11451,4191,981,0\} {11451,4191,981,0} 是满足单调递减的序列,而 { 114 , 514 , 1919 , 810 } \{114,514,1919,810\} {114,514,1919,810} 则均不满足。 ↩︎
对于一个序列 a 1 , a 2 , ⋯ , a n a_1,a_2,\cdots,a_n a1,a2,⋯,an,如果要以环形处理它,我们通常会给它增加一倍的长度,使得 a n + i = a i ( 1 ≤ i ≤ n ) a_{n+i}=a_i(1\le i\le n) an+i=ai(1≤i≤n)。这样,我们就可以轻松地处理了。 ↩︎