1.单调栈
1. 什么是单调栈?
单调栈,顾名思义,就是 具有单调性的栈 。它依旧是⼀个 栈结构 ,只不过⾥⾯存储的数据是递增或者 递减的。这种结构是很容易实现的(如下⾯的代码),但重点是维护⼀个单调栈的意义是什么?
cpp
#include <iostream>
#include <stack>
using namespace std;
const int N = 3e6 + 10;
int a[N], n;
// 单调递增栈
void test1() {
stack<int> st;
for (int i = 1; i <= n; i++) {
while (st.size() && st.top() >= a[i]) st.pop();
st.push(a[i]);
}
// 输出栈内元素(递增)
cout << "单调递增栈结果:";
while (!st.empty()) {
cout << st.top() << " ";
st.pop();
}
cout << endl;
}
// 单调递减栈
void test2() {
stack<int> st;
for (int i = 1; i <= n; i++) {
while (st.size() && st.top() <= a[i]) st.pop();
st.push(a[i]);
}
// 输出栈内元素(递减)
cout << "单调递减栈结果:";
while (!st.empty()) {
cout << st.top() << " ";
st.pop();
}
cout << endl;
}
int main() {
// 测试用例:n=5,数组a=[3,1,4,2,5]
n = 5;
a[1] = 3; a[2] = 1; a[3] = 4; a[4] = 2; a[5] = 5;
test1(); // 递增栈结果:1 2 5
test2(); // 递减栈结果:5 4 3
return 0;
}
2. 单调栈解决的问题
单调栈能帮助我们解决以下四个问题:
• 寻找当前元素左侧,离它最近,并且⽐它⼤的元素在哪;
• 寻找当前元素左侧,离它最近,并且⽐它⼩的元素在哪;
• 寻找当前元素右侧,离它最近,并且⽐它⼤的元素在哪;
• 寻找当前元素右侧,离它最近,并且⽐它⼩的元素在哪。
虽然是四个问题,但是原理是⼀致的。因此,只要解决⼀个,举⼀反三就可以解决剩下的⼏个。
3. 寻找当前元素左侧,离它最近,并且⽐它⼤的元素在哪
从左往右遍历元素,构造⼀个单调递减的栈。插⼊当前位置的元素的时:
• 如果栈为空,则左侧不存在⽐当前元素⼤的元素;
• 如果栈⾮空,插⼊当前位置元素时的栈顶元素就是所找的元素。
注意,因为我们要找的是最终结果的位置。因此,栈⾥⾯存的是每个元素的下标。
cpp
#include <iostream>
#include <stack>
using namespace std;
const int N = 3e6 + 10;
int a[N], n;
int ret[N];
void test() {
stack<int> st; // 单调递增栈:存储下标
for (int i = 1; i <= n; i++) ret[i] = 0;
// 逆序遍历:找右侧第一个更小的元素
for (int i = n; i >= 1; i--) {
// 弹出栈中≥当前值的下标
while (st.size() && a[st.top()] >= a[i]) {
st.pop();
}
// 栈非空:右侧第一个更小元素的值
if (st.size()) {
ret[i] = a[st.top()];
}
st.push(i);
}
for (int i = 1; i <= n; i++) {
cout << ret[i] << " ";
}
cout << endl;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
test();
return 0;
}
4. 寻找当前元素左侧,离它最近,并且⽐它⼩的元素在哪
从左往右遍历元素,构造⼀个单调递增的栈。插⼊当前位置的元素的时:
• 如果栈为空,则左侧不存在⽐当前元素⼩的元素;
• 如果栈⾮空,插⼊当前位置元素时的栈顶元素就是所找的元素。
注意,因为我们要找的是最终结果的位置。因此,栈⾥⾯存的是每个元素的下标。
cpp
#include <iostream> // 输入输出头文件:用来输入数字、输出结果
#include <stack> // 栈的头文件:用来使用"栈"这个工具
using namespace std;
// 定义数组最大长度,3e6+10表示足够装下大量数字(不用改)
const int N = 3e6 + 10;
int a[N], n; // a[N]:装输入的数字;n:装数字的总个数
int ret[N]; // ret[N]:装最终结果(每个数左边最近更小的数的下标)
void test() {
// 定义一个栈,栈里存的是数字的"下标"(抽屉编号),维护单调递增的栈
stack<int> st;
// 从第1个数字到第n个数字,逐个处理
for(int i = 1; i <= n; i++) {
// 核心逻辑:把栈里≥当前数字的下标都弹出(保证栈是递增的)
// st.size():判断栈里有没有东西;a[st.top()]:栈顶下标对应的数字
while(st.size() && a[st.top()] >= a[i]) {
st.pop(); // 弹出栈顶(扔掉不符合的下标)
}
// 如果栈里还有东西,栈顶就是"左边最近更小的数的下标"
if(st.size()) {
ret[i] = st.top();
} else {
ret[i] = 0; // 栈空=没找到,填0
}
// 把当前数字的下标放进栈里,维护栈的递增性
st.push(i);
}
// 输出结果:从第1个到第n个,逐个打印ret里的数
for(int i = 1; i <= n; i++) {
cout << ret[i] << " ";
}
cout << endl;
}
int main() {
// 第一步:输入数字的总个数(测试用例里是9)
cin >> n;
// 第二步:输入n个数字,依次放进a[1]到a[n](抽屉1到抽屉n)
for(int i = 1; i <= n; i++) {
cin >> a[i];
}
// 第三步:执行找"左边最近更小数下标"的逻辑
test();
return 0;
}
5. 寻找当前元素右侧,离它最近,并且⽐它⼤的元素在哪
从右往左遍历元素,构造⼀个单调递减的栈。插⼊当前位置的元素的时:
• 如果栈为空,则左侧不存在⽐当前元素⼤的元素;
• 如果栈⾮空,插⼊当前位置元素时的栈顶元素就是所找的元素。
注意,因为我们要找的是最终结果的位置。因此,栈⾥⾯存的是每个元素的下标。
cpp
#include <iostream> // 输入输出工具:能输入数字、输出结果
#include <stack> // 栈工具:用来找"右侧最近更大的数"
using namespace std;
const int N = 3e6 + 10; // 准备足够多的"抽屉"(不用改,够装数字就行)
int a[N], n; // a抽屉:装输入的9个数字;n:记数字的总个数(比如9)
int ret[N]; // ret抽屉:装最终结果(每个数的答案)
void test() {
stack<int> st; // 拿出空的"栈盒子"(维护单调递减,只存下标)
// 核心:从最后一个数往第一个数遍历(右→左)
for(int i = n; i >= 1; i--) {
// 第一步:把栈里≤当前数的下标都扔掉(保证栈是递减的)
// st.size():盒子里有东西吗?a[st.top()]:栈顶下标对应的数字
while(st.size() && a[st.top()] <= a[i]) {
st.pop(); // 扔掉不符合的下标
}
// 第二步:填结果
if(st.size()) {
ret[i] = st.top(); // 栈非空→栈顶是"右侧最近更大数的下标"
} else {
ret[i] = 0; // 栈空→没找到,填0
}
// 第三步:把当前数的下标放进栈里
st.push(i);
}
// 输出结果:从第1个到第9个,逐个打印ret抽屉里的数
for(int i = 1; i <= n; i++) {
cout << ret[i] << " ";
}
cout << endl;
}
int main() {
cin >> n; // 输入9(告诉程序有9个数)
// 把1、4、10、6、3、3、15、21、8依次放进a[1]~a[9]
for(int i = 1; i <= n; i++) {
cin >> a[i];
}
test(); // 执行找"右侧最近更大数下标"的逻辑
return 0;
}
6. 寻找当前元素右侧,离它最近,并且⽐它⼩的元素在哪
从右往左遍历元素,构造⼀个单调递增的栈。插⼊当前位置的元素的时:
• 如果栈为空,则左侧不存在⽐当前元素⼩的元素;
• 如果栈⾮空,插⼊当前位置元素时的栈顶元素就是所找的元素。
注意,因为我们要找的是最终结果的位置。因此,栈⾥⾯存的是每个元素的下标。
cpp
#include <iostream> // 输入输出工具:输入数字、输出结果
#include <stack> // 栈工具:找右侧最近更小的数
using namespace std;
const int N = 3e6 + 10; // 足够多的"抽屉",装数字和结果
int a[N], n; // a:装输入的数字;n:数字总个数
int ret[N]; // ret:装最终结果(每个数的答案)
void test() {
stack<int> st; // 空栈,维护单调递增(只存下标)
// 从最后一个数往第一个数遍历(右→左)
for(int i = n; i >= 1; i--) {
// 核心:弹出栈里≥当前数的下标(保证栈递增)
// st.size():栈里有东西吗?a[st.top()]:栈顶下标对应的数字
while(st.size() && a[st.top()] >= a[i]) {
st.pop(); // 扔掉不符合的下标
}
// 填结果:栈非空=找到答案,栈空=填0
if(st.size()) {
ret[i] = st.top();
} else {
ret[i] = 0; // 补充初始化,避免随机值
}
// 把当前数的下标放进栈,维护递增性
st.push(i);
}
// 输出结果:从第一个数到最后一个数打印
for(int i = 1; i <= n; i++) {
cout << ret[i] << " ";
}
cout << endl;
}
int main() {
cin >> n; // 输入数字个数(比如9)
// 把数字依次放进a[1]~a[n]
for(int i = 1; i <= n; i++) {
cin >> a[i];
}
test(); // 执行找数逻辑
return 0;
}
💡 总结:
• 找左侧,正遍历;找右侧,逆遍历;
• ⽐它⼤,单调减;⽐它⼩,单调增。
1.1 【模板】单调栈
题⽬来源: 洛⾕
题⽬链接: P5788 【模板】单调栈
难度系数: ★★
题目背景
模板题,无背景。
2019.12.12 更新数据,放宽时限,现在不再卡常了。
题目描述
给出项数为 n 的整数数列 a1...n。
定义函数 f(i) 代表数列中第 i 个元素之后第一个大于 ai 的元素的下标,即 f(i)=mini<j≤n,aj>ai{j}。若不存在,则 f(i)=0。
试求出 f(1...n)。
输入格式
第一行一个正整数 n。
第二行 n 个正整数 a1...n。
输出格式
一行 n 个整数表示 f(1),f(2),...,f(n) 的值。
输入输出样例
输入 #1复制
5
1 4 2 3 5
输出 #1复制
2 5 4 5 0
说明/提示
【数据规模与约定】
对于 30% 的数据,n≤100;
对于 60% 的数据,n≤5×103 ;
对于 100% 的数据,1≤n≤3×106,1≤ai≤109。
【解法】
右侧离它最近并且⽐它⼤的元素:
• 逆序遍历数组;
• 构造⼀个单调递减的栈;
• 进栈时,栈顶元素就是最终结果。
【参考代码】
cpp
#include <iostream> // 输入输出工具:输入数字、输出结果
#include <stack> // 栈工具:找右侧第一个更大的数
using namespace std;
const int N = 3e6 + 10; // 足够多的"抽屉",装数字和结果(适配3e6的大数据)
int n; // 数列的长度(比如示例里的5)
int a[N]; // a抽屉:装输入的数列(a[1]=1, a[2]=4...)
int ret[N]; // ret抽屉:装每个数的答案(右边第一个更大数的下标,没找到填0)
int main() {
// 第一步:输入数列长度和数列本身
cin >> n;
for(int i = 1; i <= n; i++) {
cin >> a[i]; // 把数字依次放进a[1]到a[n]
}
stack<int> st; // 空栈:维护单调递减,只存"下标"(抽屉号)
// 第二步:逆序遍历(从最后一个数到第一个数)
for(int i = n; i >= 1; i--) {
// 核心:弹出栈里≤当前数的下标(保证栈是递减的)
// st.size():栈里有东西吗?a[st.top()]:栈顶下标对应的数字
while(st.size() && a[st.top()] <= a[i]) {
st.pop(); // 扔掉不符合的下标
}
// 第三步:填答案
if(st.size()) {
ret[i] = st.top(); // 栈非空→栈顶是"右边第一个更大数的下标"
} else {
ret[i] = 0; // 栈空→没找到,填0
}
// 第四步:把当前数的下标放进栈,维护递减性
st.push(i);
}
// 第五步:输出结果(如果要输出"值",把ret[i]改成a[ret[i]]即可)
for(int i = 1; i <= n; i++) {
// 示例输出是下标,所以输出ret[i];若要输出值,改成cout << a[ret[i]] << " ";
cout << ret[i] << " ";
}
cout << endl;
return 0;
}
1.2 发射站
题⽬来源: 洛⾕
题⽬链接: P1901 发射站
难度系数: ★★
题目描述
某地有 N 个能量发射站排成一行,每个发射站 i 都有不相同的高度 Hi,并能向两边(两端的发射站只能向一边)同时发射能量值为 Vi 的能量,发出的能量只被两边最近的且比它高的发射站接收。显然,每个发射站发来的能量有可能被 0 或 1 或 2 个其他发射站所接受。
请计算出接收最多能量的发射站接收的能量是多少。
输入格式
第 1 行一个整数 N。
第 2 到 N+1 行,第 i+1 行有两个整数 Hi 和 Vi,表示第 i 个发射站的高度和发射的能量值。
输出格式
输出仅一行,表示接收最多能量的发射站接收到的能量值。答案不超过 32 位带符号整数的表示范围。
输入输出样例
输入 #1复制
3
4 2
3 5
6 10
输出 #1复制
7
说明/提示
对于 40% 的数据,1≤N≤5000,1≤Hi≤105,1≤Vi≤104。
对于 70% 的数据,1≤N≤105,1≤Hi≤2×109,1≤Vi≤104。
对于 100% 的数据,1≤N≤106,1≤Hi≤2×109,1≤Vi≤104。
【解法】
有了单调栈之后,这道题就变成模拟题了......
【参考代码】
cpp
#include <iostream> // 输入输出工具:输入发射站信息、输出结果
#include <stack> // 栈工具:找"最近更高的站"
using namespace std;
typedef long long LL; // 防止能量值太大溢出(比如多个能量相加超过int范围)
const int N = 1e6 + 10; // 足够多的"抽屉",装1e6个发射站的信息
int n; // 发射站的总数(比如示例里的3)
LL h[N], v[N]; // h:装每个站的高度;v:装每个站的能量值
LL sum[N]; // sum:装每个站接收的总能量(初始都是0)
int main() {
// 第一步:输入发射站数量和每个站的高度、能量
cin >> n;
for(int i = 1; i <= n; i++) {
cin >> h[i] >> v[i]; // 站1的h=4、v=2;站2的h=3、v=5;站3的h=6、v=10
}
// 第二步:找每个站"左边最近更高的站",并把能量加给这个站
stack<int> st; // 空栈:维护单调递减(只存站的下标)
for(int i = 1; i <= n; i++) { // 从左到右遍历每个站
// 核心:弹出栈里≤当前站高度的下标(保证栈是递减的)
while(st.size() && h[st.top()] <= h[i]) {
st.pop();
}
// 栈非空:栈顶就是"左边最近更高的站",接收当前站的能量
if(st.size()) {
sum[st.top()] += v[i]; // 比如站2的左边更高是站1,sum[1] +=5
}
// 把当前站的下标放进栈,维护递减性
st.push(i);
}
// 第三步:找每个站"右边最近更高的站",并把能量加给这个站
while(st.size()) st.pop(); // 清空之前的栈
for(int i = n; i >= 1; i--) { // 从右到左遍历每个站
// 核心:弹出栈里≤当前站高度的下标(保证栈是递减的)
while(st.size() && h[st.top()] <= h[i]) {
st.pop();
}
// 栈非空:栈顶就是"右边最近更高的站",接收当前站的能量
if(st.size()) {
sum[st.top()] += v[i]; // 比如站1的右边更高是站3,sum[3] +=2
}
// 把当前站的下标放进栈,维护递减性
st.push(i);
}
// 第四步:找接收能量最多的站
LL ret = 0; // 存最大能量值(初始0)
for(int i = 1; i <= n; i++) {
ret = max(ret, sum[i]); // 逐个比较,保留最大的
}
cout << ret << endl; // 输出7(示例)
return 0;
}