「数据结构详解·十」双端队列 & 单调队列的初步


1. 双端队列及其实现

众所周知,队列的两端是一端进,一端出。而双端队列(Double-ended queue, Deque),则是两端均可压入、弹出。压入操作分为 push_front()push_back(),弹出操作也分为 pop_front()pop_back()
如果你学了队列的话会更好理解。没学过队列可以看「数据结构详解·四」队列学习队列。
我们主要讲讲双端队列的实现。

1-1. 数组模拟

和队列类似,不再赘述。有时不建议用。

1-2. C++ STL deque

C++ STL 为我们提供了 deque 来实现双端队列。操作函数就是上面所说的,这里也不再赘述。
当然,STL deque 是可以和 STL vector 一样进行遍历的。
不过,STL deque 不管是时间还是空间,都具有巨大常数,在某些情况下不建议使用。
比如,Luogu B3656 【模板】双端队列 1
我们可以很快地写出使用 deque 的代码。
然而,它 MLE 了。
因为它的巨大常数。
那该怎么办?

1-3. C++ STL list

众所周知,这是实现的双向链表。但是仔细想想,如果我们不去遍历双端队列的值,是不是就相当于一个弱化的双向链表?正是因为链表无法直接访问值,所以我们可以用 STL list 代替 STL deque。
参考代码:

#include<bits/stdc++.h>
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;
}

“看起来好像双端队列并没有什么用处啊……”
你别急,看看标题。
是的,单调队列就是双端队列的最重要的应用。

2. 单调队列的实现及初步应用

单调队列(Monotone queue),顾名思义,就是数据具有单调性1的队列
单调队列可以优化许多算法——本篇文章主要讲滑动区间最值的单调队列优化。

2-1. Luogu P1886 滑动窗口 /【模板】单调队列

这是一道经典的单调队列。
如果我们考虑枚举每个区间,对于每个区间去挨个计算的话,时间复杂度约为 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<bits/stdc++.h>
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)

2-2. Luogu P2629 好消息,坏消息

如果我们直接枚举 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+n1] ,对于 ∀ i ∈ [ k , k + n − 1 ] \forall i\in[k,k+n-1] i[k,k+n1],满足 ∑ j = k i A j ≥ 0 \sum\limits_{j=k}^iA_j\ge0 j=kiAj0
区间肯定是要枚举的,想想怎么满足后面的条件?
是不是只要满足区间内最小的 ∑ j = k i A j ≥ 0 \sum\limits_{j=k}^iA_j\ge0 j=kiAj0 即可?
想到了什么?
对!单调队列!
我们只要做一个区间前缀和最小值的滑动窗口即可!
参考代码:

#include<bits/stdc++.h>
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;
}

3. 巩固练习


  1. 也就是这串序列满足单调递(不)增或递(不)减,比如 { 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} 则均不满足。 ↩︎

  2. 对于一个序列 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(1in)。这样,我们就可以轻松地处理了。 ↩︎

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值