单调栈与单调队列
引入
有时我们希望求出往前第一个比自己大的数。
形式化的说:给一个数组 \(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\) 必须出队的时间。维护单调队列时每次判断队首元素是否需要出队即可。注意单调队列是双端队列,需要队首出,队尾进出。
代码
单调栈
#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; }
单调队列
#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; }
习题
板子就不列了。
-
P1901 发射站 - 洛谷 较板。
-
P7167 [eJOI2020 Day1] Fountain - 洛谷 很妙的一题,需要结合其他算法。
-
P2422 良好的感觉 - 洛谷 依然很妙。需要巧妙地枚举。
-
P1823 [COI2007] Patrik 音乐会的等待 - 洛谷 没做过,有时间做一下。
后记
如果是单调不降或上升等同理。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!