栈 是实现机器递归(深度优先搜索)的基本结构
0、AcWing 41. 包含min函数的栈
题意 :
设计一个支持push,pop,top等操作并且可以在O(1)时间内检索出最小元素的堆栈。
push(x)–将元素x插入栈中
pop()–移除栈顶元素
top()–得到栈顶元素
getMin()–得到栈中最小元素
class MinStack {
public:
/** initialize your data structure here. */
stack<int> stk1, stk2;
MinStack() {
}
void push(int x) {
stk1.push(x);
if (stk2.empty() || stk2.top() >= x) {
stk2.push(x);
}
}
void pop() {
if (stk2.top() == stk1.top()) {
stk2.pop();
}
stk1.pop();
}
int top() {
return stk1.top();
}
int getMin() {
return stk2.top();
}
};
/**
* Your MinStack object will be instantiated and called as such:
* MinStack obj = new MinStack();
* obj.push(x);
* obj.pop();
* int param_3 = obj.top();
* int param_4 = obj.getMin();
*/
1、AcWing 128. 编辑器
题意 :
- 1、I x,在光标处插入数值 x。
- 2、D,将光标前面的第一个元素删除,如果前面没有元素,则忽略此操作。
- 3、L,将光标向左移动,跳过一个元素,如果左边没有元素,则忽略此操作。
- 4、R,将光标向右移动,跳过一个元素,如果右边没有元素,则忽略此操作。
- 5、Q k,假设此刻光标之前的序列为 a1,a2,…,an,输出 max1≤i≤kSi,其中 Si=a1+a2+…+ai。
- 每一个 Q k 指令,输出一个整数作为结果,每个结果占一行。
思路 :
- 本题的特殊点在于,四种操作都在光标位置发生,并且操作完成后光标至多移动1个位置,根据这种“始终在序列中间某个指定位置进行修改”的性质,再联想上一章中我们动态维护中位数的“对顶堆”算法,不难想到一个类似的“对顶栈”做法
- 建立两个栈,一个栈是光标左边,另一个栈是光标右边,且二者都以光标所在的那一段为栈顶
- 在每次有数加入左边的栈(包括I和R)时,都需要更新前缀和以及f函数
#include <iostream>
#include <climits>
using namespace std;
const int N = 1e6 + 10;
int stkl[N], stkr[N];
int topl, topr;
int sum[N], f[N];
void add(int x) {
stkl[ ++ topl] = x;
sum[topl] = sum[topl - 1] + x;
f[topl] = max(f[topl - 1], sum[topl]);
}
int main() {
int q; cin >> q;
char ops[2];
f[0] = INT_MIN;
while (q -- ) {
int x;
scanf("%s", ops);
if (*ops == 'I') {
scanf("%d", &x);
add(x);
} else if (*ops == 'D') {
if (topl) topl -- ;
} else if (*ops == 'L') {
if (topl) stkr[ ++ topr] = stkl[topl -- ];
} else if (*ops == 'R') {
if (topr) add(stkr[topr -- ]);
} else {
scanf("%d", &x);
cout << f[x] << endl;
}
}
}
进出栈顺序问题
题意 :
- 给定一个1~N这N个整数和一个无限大的栈,每个数都要进栈并出栈一次,如果进栈的顺序为1,2,…,N,那么可能的出栈顺序有多少种
思路一:搜索 O ( 2 N ) O(2^N) O(2N)
- 面对任何一个状态我们只有两种选择
1、下一个数进栈
2、如果栈非空,栈顶出栈 - 因此,可以枚举每一步如何选择,用递归实现这个规模为 O ( 2 N ) O(2^N) O(2N)的枚举
思路二:递推 O ( N 2 ) O(N^2) O(N2)
- 由于只关心有多少种而不关心具体方案,因此我们使用 递推 直接统计
- 设 S N S_N SN表示进栈顺序为1,2,…,N时可能的出栈顺序总数,根据递推的理论,我们需要想办法把问题分解成若干个相似的子问题
- 考虑“1”这个数排在最终出栈序列中的位置,如果最后“1”排在第k个,那么整个序列的进出栈过程就是:
1、整数1入栈
2、整数2~k这k-1个数按某种顺序进出栈
3、整数1出栈,排在第k个
4、整数k+1~N这N-k个数按某种顺序进出栈 - 于是整个问题就被“1”这个数划分成了“k-1个数进出栈“和”N-k个数进出栈“这两个子问题,得到递推公式:
S N = ∑ k = 1 N S k − 1 ∗ S N − k S_N=\sum_{k=1}^{N}S_{k-1}*S_{N-k} SN=∑k=1NSk−1∗SN−k(注意是相乘)
思路三:动态规划 O ( N 2 ) O(N^2) O(N2)
- 在任何一个时刻,我们实际上只关心有多少个数尚未入栈,有多少个数还在栈里,并进一步做出合法的操作,而不关心这些数具体是哪些
- 因此,我们可以用 F [ i , j ] F[i,j] F[i,j]表示有i个数尚未入栈,有j个数还在栈里,已经有n-i-j个数出栈时的 方案总数
- 在最终状态下,即所有数已经出栈时,顺序已经确定,所以边界是 F [ 0 , 0 ] = 1 F[0,0]=1 F[0,0]=1
- 我们需要求出初始状态下,即所有数尚未进栈时,可以到达上述边界的方案总数,所以目标为 F [ N , 0 ] F[N,0] F[N,0]
- 每一步的两种决策分别是“把一个数进栈”和“把栈顶的数出栈”,所以有公式:
F [ i , j ] = F [ i − 1 , j + 1 ] + F [ i , j − 1 ] F[i,j]=F[i-1,j+1]+F[i,j-1] F[i,j]=F[i−1,j+1]+F[i,j−1]
思路四:数学 O ( N ) O(N) O(N)
- 该问题等价于求第N项Catalan数
1、AcWing 129. 火车进栈
题意 :
- 这 n 列火车按 1 到 n 的顺序
- 也就是说这个火车站其实就相当于一个栈,每次可以让右侧头火车进栈,或者让栈顶火车出站。
- 现在请你按《字典序》输出前 20 种可能的出栈方案。
- 输入一个整数 n,代表火车数量。
- 1≤n≤20
思路:
- 即使是最后一种情况也要回溯
- u代表当前等待进栈的是
- 因为需要一共20种,因此remain=20,且进入dfs后第一个条件判断为remain
- 当路径大小为n时,输出,并更新remain,return
#include <iostream>
#include <vector>
#include <stack>
using namespace std;
int n;
vector<int> path;
stack<int> stk;
int remain = 20;
void dfs(int u) {
if (!remain) return ;
if (path.size() == n) {
for (int i : path) cout << i;
cout << endl;
remain -- ;
return ;
}
if (stk.size()) {
path.push_back(stk.top());
stk.pop();
dfs(u);
stk.push(path.back());
path.pop_back();
}
if (u <= n) {
stk.push(u);
dfs(u + 1);
stk.pop();
}
}
int main() {
cin >> n;
dfs(1);
}
2、(跳)AcWing 130. 火车进出栈问题
题意 :
- 一列火车 n 节车厢,依次编号为 1,2,3,…,n。
- 每节车厢有两种运动方式,进栈与出栈,问 n 节车厢出栈的可能排列方式有多少种。
- 1≤n≤60000
表达式计算
栈的一大用处是做算术表达式的运算。算术表达式通常有前缀、中缀、后缀三种表示方法
- 中缀表达式:是我们最常见的表达式
- 前缀表达式:又称 波兰式,形如
op A B
,其中,A和B是另外两个前缀表达式,例如* 3 - 1 2
- 后缀表达式:又称逆波兰式,形如
A B op
,例如1 2 - 3 *
前缀和后缀表达式的值的定义是,先递归求出A,B的值,二者再做op运算的结果(因此对于后缀表达式而言,op是从左到右的;而对于前缀表达式,op是从右到左的)
这两种表达式不需要使用括号,其运算方案是唯一确定的
对于计算机来说,它最容易理解后缀表达式,我们可以用栈
O
(
N
)
O(N)
O(N)求出它的值
1、后缀表达式求值 O(N)
1、建立一个用于 存数 的栈,逐一扫描该后缀表达式中的元素
(1)如果遇到一个数,则把该数入栈
(2)如果遇到运算符,就取出栈顶的两个数进行运算,把结果入栈
2、扫描完成后,栈中恰好剩下一个数,就是该后缀表达式的值
2、中缀表达式转后缀表达式 O(N)
1、建立一个用于 存运算符 的栈,逐一扫描该中缀表达式中的元素
(1)如果遇到一个数,输出该数
(2)如果遇到左括号,把左括号入栈
(3)如果遇到右括号,不断取出栈顶并输出,直到栈顶为左括号,然后把左括号出栈
(4)如果遇到运算符,只要栈顶符号的优先级不低于新符号,就不断取出栈顶并输出,最后把新符号入栈,优先级为乘除>加减>左括号(因此,在栈中,从栈顶到底部运算符优先级降低)
2、依次取出并输出栈中的所有剩余符号,最终输出的序列就是一个与原中缀表达式等价的后缀表达式
以上例子中包含的都是一位数,如果是多位数,并且表达式是使用字符串逐字符存出的,我们只需要稍加判断,把连续的一段数字看成一个数即可
当然,我们也可以不转化成后缀表达式,而是使用递归法直接求解中缀表达式的值,时间复杂度为 O ( N 2 ) O(N^2) O(N2)
3、中缀表达式的递归法求值 O ( N 2 ) O(N^2) O(N2)
目标:求解中缀表达式S[1~N]的值
子问题:求解中缀表达式S的子区间表达式S[L~R]的值
1、在L~R中考虑 没有被任何括号包含 的运算符:
(1)若存在加减号,选其中最后一个,分成左右两半递归,结果相加减,返回
(2)若存在乘除号,选其中最后一个,分成左右两半递归,结果相乘除,返回
2、若不存在没有被任何括号包含的运算符:
(1)若首尾字符是括号,递归求解S[L+1~R-1],把结果返回
(2)否则,说明区间S[L~R]是一个数,直接返回数值
单调栈
1、AcWing 131. 直方图中最大的矩形(边界处理)
题意 :
- 直方图是由在公共基线处对齐的一系列矩形组成的多边形。
- 矩形具有相等的宽度,但可以具有不同的高度。
- 现在,请你计算在公共基线处对齐的直方图中最大矩形的面积。
思路 :
- 首先考虑暴力做法,以每个矩形的高度为准,向两边扩展,直到遇到比它矮的为止
- 如图所示,记录每个矩形向两侧扩展的边界 [l,r],它扩展出的矩形面积为 s=(r−l+1)∗hs=(r−l+1)∗h,最优解会在这些扩展出的矩形中产生。时间复杂度 O(n2),每个矩形向两侧扩展的最大宽度为矩形个数 nn,共进行 nn 次这样的操作。
- 单调栈优化:在计算每个矩形可以扩展的左边界时,可以发现有一些矩形是可以不考虑的
- 如图所示,由于2号矩形的存在,在计算2右边的矩形的左边界时,可以不考虑1号矩形。
- 高度高于2号的矩形会被2卡住,高度小于等于2号的也必然小于等于1号。
- 观察可知,在计算左边界时,靠左的且较高的矩形可以省略,因此可以用单调栈优化。同理,在计算右边界时,靠右的且较高的矩形可以省略
- q[tt]作为栈顶元素下标,当满足h[q[tt]] >= h[i]时,弹出栈顶
- l[i], r[i]表示第i个矩形的高度可向两侧扩展的左右边界(包含本身)
- 边界处理!!!:
(1)每个矩形的高度都是>=0的,为了使得每个矩形的两侧都有矮于它的矩形,所以往两侧放了两个-1的矩形h[0] = h[n + 1] = -1;
(2)由于有-1矩形的存在,不会有任何矩形的高度 hh 满足 −1>=h,所以栈不会空,因此可以省略栈中是否有元素的判断条件tt >= 0
- 这就是著名的单调栈算法,时间复杂度为O(N)。借助单调性处理问题的思想在于及时排除不可能的选项,保持策略集合的高度有效性和秩序性,从而为我们做出决策提供更多的条件和可能方法。
#include <iostream>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int n, h[N];
int l[N], r[N];
int q[N], tt;
int main() {
while (scanf("%d", &n), n) {
for (int i = 1; i <= n; ++ i) scanf("%d", &h[i]);
h[0] = h[n + 1] = -1;
tt = -1;
q[ ++ tt] = 0;
for (int i = 1; i <= n; ++ i) {
while (h[q[tt]] >= h[i]) -- tt;
l[i] = i - q[tt];
q[ ++ tt] = i;
}
tt = -1;
q[ ++ tt] = n + 1;
for (int i = n; i; -- i) {
while (h[q[tt]] >= h[i]) -- tt;
r[i] = q[tt] - i;
q[ ++ tt] = i;
}
ll mx = 0;
for (int i = 1; i <= n; ++ i) {
ll now = (ll)h[i] * (l[i] + r[i] - 1);
mx = max(mx, now);
}
printf("%lld\n", mx);
}
}