单调栈与单调队列

引入

有时我们希望求出往前第一个比自己大的数。

形式化的说:给一个数组 \(a\),求一个数组 \(p\),使得 \(a_{p(i)}>a_i\)\(\forall p_i<j<i,a_j\le a_i\)。若不存在 \(p_i\)\(p_i\gets i\)

怎么求呢?

暴力

首先考虑最朴素的做法。对于每一个 \(i\),向前枚举 \(j<i\),若 \(a_j>a_i\)\(p_i\gets j\)

\(a\) 降序时,复杂度达到上界,为 \(O(n^2)\),不够优秀。

单调栈

聪明的算法经常都是优化\观察暴力得来的,于是观察暴力出的 \(p\) 数组。有以下例子:

id \(1\) \(2\) \(3\) \(4\) \(5\)
\(a\) \(3\) \(2\) \(4\) \(1\) \(1\)
\(p\) \(1\) \(1\) \(3\) \(3\) \(3\)

可以发现,\(p_1=p_2=1\),而到 \(p_3\) 时却为 \(3\)。为什么呢?因为 \(a_3>a_1\),于是 \(p_3=p_4=p_5=3\),都找 \(3\) 去了。

可以看下面这张图理解:

问题来了,如果只挡住一个,后面山外有山有更高的怎么办?维护一个栈即可,我们叫他单调栈(因为栈中单调)。

操作

可以将这个单调栈 \(stk\) 想象为 OI 队。栈中保存 \(a\) 中下标即可。其中满足 \(1\le i<n,stk_i<stk_{i+1}, a_{stk(i)}>a_{stk(i+1)}\)。即年龄从大到小(下标从小到大),实力也从大到小。每次添加一个 \(a_i\),都要卷死 \(stk\) 中的幸运 oier。

  • 出栈。如果有学长比你(\(i\))弱,ta 就可以 AFO 了。(\(a_{stk(back)}\le a_i\),那么退栈。)
  • 入栈。如果学长们都比你强,你就淘汰不了 ta 们,于是你(\(i\))入栈。

这样就保证了 OI 队中实力单调下降。

那么讲了半天 \(p_i\) 怎么求?注意到入栈时学长都比你强,那么 ta 们中最小的就是所求的 \(p_i\)

由于每个 \(a_i\) 顶多入栈、出栈各一次,故时间复杂度 \(O(n)\)

单调队列

其实相比单调栈没改多少。注意到每个 oier 迟早得退役,那么有时可能会规定 \(a_i\) 必须出队的时间。维护单调队列时每次判断队首元素是否需要出队即可。注意单调队列是双端队列,需要队首出,队尾进出。

代码

单调栈

P5788 【模板】单调栈 - 洛谷

#include <iostream>
#include <vector>
using namespace std;
int main()
{
	int n;
	scanf("%d",&n);
	vector<int> v;
	int a[n+1],f[n+1];// 以前的代码好抽象,我现在都改到 main 外开数组了。
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
	}
	for(int i=n;i>=1;i--)
	{
		while(!v.empty()&&a[i]>=a[v.back()])// 如果比我大(或等于)
			v.pop_back();// 弹出
		f[i]=(v.empty()?0:v.back());// 如果没有比我小,答案为 0(题目中定义)
		v.push_back(i);
	}
	for(int i=1;i<=n;i++)
	{
		printf("%d ",f[i]);
	}
	return 0;
}

单调队列

P1886 滑动窗口 /【模板】单调队列 - 洛谷

#include <iostream>
#include <deque>
using namespace std;
int main()
{
	int n,k;
	scanf("%d %d",&n,&k);
	int a[n+1];
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
	}
	deque<pair<int,int>> d;// 其实尽量少用 deque。可以不用 pair。(是以前写的代码。)
	for(int i=1;i<=n;i++)// 求最小值
	{
		while(!d.empty()&&d.back().second>=a[i])// 如果大等于我,弹出
			d.pop_back();
		if(!d.empty()&&d.front().first<=i-k)// 如果过时,弹出
			d.pop_front();
		d.push_back({i,a[i]});// 入队
		if(i>=k)
			printf("%d ",d.front().second);// 输出答案。若没有比我小的,答案就是自己
	}
	puts("");
	d.clear();
	for(int i=1;i<=n;i++)// 求最大值
	{
		while(!d.empty()&&d.back().second<=a[i])// 同理
			d.pop_back();
		if(!d.empty()&&d.front().first<=i-k)
			d.pop_front();
		d.push_back({i,a[i]});
		if(i>=k)
			printf("%d ",d.front().second);
	}
	return 0;
}

习题

板子就不列了。

后记

如果是单调不降或上升等同理。

posted @ 2023-10-31 22:11  Po7ed  阅读(5)  评论(0编辑  收藏  举报