单调栈 单调队列
单调栈
单调栈:基于栈的数据结构,栈中数据从栈底至栈顶具有单调性,序列中每个元素都必须要进入一次单调栈。由于单调性,序列元素无需时刻都保留在单调栈内。
应用:求解 N G E / N L E NGE/NLE NGE/NLE、 P G E / P L E PGE/PLE PGE/PLE类问题(序列中下一个/上一个 更大/更小的元素),RMQ问题
- 更大元素问题( G r e a t e r Greater Greater):构造单调递减栈;更小元素问题( L e s s Less Less):构造单调递增栈。原因在于即将入栈元素一定是要对当前栈顶元素的答案没有贡献,所以才能让其入栈。
- 序列上一个类问题(
P
r
e
v
i
o
u
s
)
Previous)
Previous):即将入栈元素为主导,看栈顶元素(往前看)。修改的是即将入栈元素。不允许栈顶元素与即将入栈元素相同(入栈前必须将栈中与其相同元素全部岀栈)。
序列下一个类问题( N e x t Next Next):栈顶元素为主导,看即将入栈元素(往后看)。修改的是栈顶元素。允许即将入栈元素与栈顶元素相同。 - 总结:构造的单调性与求解问题相反,方向与求解问题相同,上一个不允许同,下一个允许同
N G E / N L E NGE/NLE NGE/NLE问题
具有相同答案元素具有连续性与单调性。暴力法的核心瓶颈在于,对于具有重复答案的部分进行了大量的反复比较。
优化思路:
- 批量处理:具有相同答案的元素具有连续性
- 动态维护:一旦元素确定答案,其将不可能改变答案,也不可能再成为其他元素的答案,其不再被考察
- 单调性:具有相同答案的元素具有单调性遍历序列时动态维护单调子序列,作为待确定答案元素的集合。若新元素破坏了子序列单调性,则其对单调子序列末尾元素答案确定产生了贡献,其必定为至少一个元素的答案。这个动态维护的集合就是单调栈。
NGE/NLE问题中,单调栈用于暂存未确定答案的元素,用于这些元素的批量确定答案。一旦有比其更大/小的元素进入栈,则其答案确定,该元素立即出栈。
核心思想是栈顶元素为主导,看即将入栈元素。修改的是栈顶元素。相同元素一定不会产生答案贡献,可直接入栈。允许即将入栈元素与栈顶元素相同。确定答案为当单调性被破坏时,发生在该元素出栈时。
vector<pair<int,int>>v;//存储本身及其下标
vector<int>ans;
stack<int>s;//存储下标
void nge(){
for(int i=0;i<v.size();i++){
while(s.size()&&v[s.top()].first<v[i].first)//NLE为> 允许栈顶元素和即将入栈元素相同
ans[v[s.top()].second]=i,s.pop();
s.push(i);
}
}
P G E / P L E PGE/PLE PGE/PLE问题
具有相同答案元素具有连续性与单调性。考虑动态维护单调子序列,由于答案位于元素前方,因此该序列是作为潜在答案的集合。若新元素破坏了单调性,则其比至少一个元素在位置(更靠右)和大小上都更优,则其更适合作为答案,将失去对答案贡献的元素移除序列。这其实就是单调栈。
PGE/PLE问题中,单调栈用于潜在答案的集合,用于为之后的元素批量确定答案,潜在答案呈单调递减性。
入栈元素:在距离上离后续元素更近,且对后续元素有潜在的答案贡献。
出栈元素:一旦有比其更优(两个维度:(1)距离 (2)元素大小)的答案入栈,其对之后的元素便失去贡献,其立即出栈。
核心思想是以即将入栈元素为主导,看栈顶元素。修改的是即将入栈元素。
不允许栈顶元素与即将入栈元素相同。考虑对后续元素的影响:相同元素中最后一个距离后续元素更近,因此只有最后一个可能会成为后续元素的答案;考虑对相同元素自身的影响:只能访问栈顶元素。若相同元素保留在栈内,会导致只有第一个元素能获取到答案,其他相同元素只能访问到第一个相同元素。显然这不是其答案,会导致遗漏。
vector<pair<int,int>>v;//存储本身及其下标
vector<int>ans;
stack<int>s;//存储下标
void pge(){
for(int i=0;i<v.size();i++){
while(s.size()&&v[i].first>=v[s.top()].first) s.pop();PLE为<= 不允许栈顶元素与入栈元素相同
if(s.size()) ans[v[i].second]=s.top();
s.push(i);
}
}
总结
下一个更大(小)元素问题 | 上一个更大(小)元素问题 | |
---|---|---|
单调栈作用 | 待确定答案的元素的集合 | 对之后元素答案的确定存在潜在贡献集合 |
入栈元素 | 待确定答案的元素 | 该元素能对序列之后元素答案确定产生潜在贡献 |
出栈元素 | 该元素答案已完成确定 | 该元素已无法对之后元素答案确定产生潜在贡献 |
相同元素是否可入栈 | 是 | 否 |
答案确定时机 | 元素出栈 | 元素入栈 |
方向 | 栈顶元素为主导,看即将入栈元素 | 即将入栈元素为主导,看栈顶元素 |
求解更大元素问题 | 求解更小元素问题 | |
---|---|---|
顺序遍历 | 构造单调递减栈 | 构造单调递增栈 |
逆序遍历 | 构造单调递增栈 | 构造单调递减栈 |
RMQ问题
-
定义若出现重复元素,则管理区间归属右侧的元素。对于每个元素 v i v_i vi,预处理其 RMQ \texttt{RMQ} RMQ管理区间 ( L ′ , R ′ ) (L',R') (L′,R′):以区间最小值为例:
-
R ′ R' R′:计算 v i v_i vi的下个 ≤ v i \le v_i ≤vi的位置
-
L ′ L' L′:计算 v i v_i vi上个 < v i <v_i <vi的位置
则 v i v_i vi是 ( L ′ , R ′ ) (L',R') (L′,R′)区间内的最小值。
-
-
离线+单调栈思想:
- 将询问离线:借助桶排序的思想,对区间右端点 R i R_i Ri维护长度为 1 1 1的桶,左端点 L i L_i Li加入桶内并排序,统计出所有 R i R_i Ri相同的区间。桶排序后,所有右端点 R i R_i Ri具有单调递增性,每个桶内的 L i L_i Li也具有单调递增性。
- 枚举每个桶:固定右端点
R
R
R,序列前
R
R
R个数构成潜在最值集合。桶内
L
i
L_i
Li呈单调递增性,最值的变化也呈单调性。因此借助单调栈的思想,维护一个前
R
R
R个元素的单调子序列。对于
max
\texttt{max}
max值,构造单调递减子序列;对于
min
\texttt{min}
min值,构造单调递增子序列。首个下标
≥
L
i
\ge L_i
≥Li的元素即为答案。
算法流程:
定义单调存储的是下标:
- 对于所有询问 [ L 1 , R 1 ] , ⋯ , [ L n , R n ] [L_1,R_1],\cdots,[L_n,R_n] [L1,R1],⋯,[Ln,Rn],对所有右端点 R i R_i Ri维护长度为 1 1 1的桶,进行排序。排序后,所有右端点 R i R_i Ri具有单调递增性,每个桶内的 L i L_i Li也具有单调递增性;
- 对于询问 [ L i , R i ] [L_i,R_i] [Li,Ri],遍历序列到 R i R_i Ri,维护前 R i R_i Ri个元素的单调子序列,作为潜在最值元素集合;
- 二分查找单调子序列中首个 ≥ L i \ge L_i ≥Li的下标,即为区间 [ L i , R i ] [L_i,R_i] [Li,Ri]的最值。
时间复杂度 O ( n + q log 2 n ) O(n+q\log_2n) O(n+qlog2n)。
单调队列
单调队列:基于双端队列的数据结构,队列中数据从队头到队尾具有单调性。要求遍历的序列中每个元素都必须要进入一次单调队列。由于其单调性,不要求每个元素在遍历序列之后仍然保留在单调队列内。
单调队列的性质:
-
队头始终为管理区间内的最值。
-
由于需要对队列进行队尾入队,队头、队尾出队,故采用双端队列(deque)实现。
单调队列最常用于优化滑动窗口。
应用:滑动窗口区间RMQ(最值)问题、滑动窗口最大子序和问题…
滑动窗口区间RMQ问题
滑动窗口利用双指针的思想,是尺取法的应用。滑动窗口必须保证每个元素都能入队,由于滑动窗口的滑动特性,不要求每个元素都持续在队列中)
- 最大值问题:维护单调递减队列;最小值问题:维护单调递增队列。其中队头一定是最值
本类问题中,单调队列只能从队尾入队,从队头、队尾出队
关键操作:
- 删头:若队头元素脱离窗口(超过窗口范围),队头元素从队头出队
- 去尾:若新元素在从队尾入队时,原队尾破坏了队列的单调性,使原队尾出队
int k;//窗口长度
vector<int>a;//下标从1开始
deque<int>dq;//以单增队列为例,存储为下标
void solve(){
for(int i=1;i<=a.size();i++){
while(dq.size()&&a[dq.back()]>=a[i]) dq.pop_back();//单减队列为<=
dq.push_back(i);
if(i>=k){//窗口大小已取足
while(dq.size()&&i-dq.front()>=k) dq.pop_front();//不可写作m.size()>=k(单调队列元素下标可能非连续)
cout<<a[m.front()]<<' ';
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通