栈是一种 “后进先出” 的线性数据结构。
在 C++ 可以使用 STL
里的 stack
,也可以使用数组模拟栈。通常情况使用 stack
就可以满足操作需求,不过有时需要灵活地操作栈的时候,使用数组模拟要方便的多。比如 双向排序 。
书上介绍了两个使用栈的例子。
【例题】包含min函数的栈
设计一个支持push,pop,top等操作并且可以在O(1)时间内检索出最小元素的堆栈。
- push(x)–将元素x插入栈中
- pop()–移除栈顶元素
- top()–得到栈顶元素
- getMin()–得到栈中最小元素
分析:
第一次遇到使用栈时还要查询栈中最小值的。结合前面题目对顶堆的用法,我就在想既然一个栈是不可能维护这么多信息,那使用两个栈不就可以吗。于是另外一个栈就用来维护当前栈中最小值。
在每次 push
操作时,我们在另一个栈中 push
的元素就为 它当前栈顶与即将压入的数据取最小值压入。这样题目中所有的操作就都是
O
(
1
)
O(1)
O(1) 完成的。
代码如下:
class MinStack {
public:
/** initialize your data structure here. */
int stk[101], minstk[101];
int tt = -1;
MinStack() {
}
void push(int x) {
if(tt == -1) {
minstk[++tt] = x;
stk[tt] = x;
} else {
minstk[tt + 1] = min(minstk[tt], x);
stk[++tt] = x;
}
}
void pop() {
tt--;
}
int top() {
return stk[tt];
}
int getMin() {
return minstk[tt];
}
};
/**
* 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();
*/
【例题】编辑器
你将要实现一个功能强大的整数序列编辑器。
在开始时,序列是空的。
编辑器共有五种指令,如下:
I x
,在光标处插入数值 x x x 。D
,将光标前面的第一个元素删除,如果前面没有元素,则忽略此操作。L
,将光标向左移动,跳过一个元素,如果左边没有元素,则忽略此操作。R
,将光标向右移动,跳过一个元素,如果右边没有元素,则忽略此操作。Q k
,假设此刻光标之前的序列为 a 1 , a 2 , ⋯ , a n a_1,a_2,\cdots,a_n a1,a2,⋯,an ,输出 m a x 1 ≤ i ≤ k S i max_{1\le i\le k}S_i max1≤i≤kSi,其中 S i = a 1 + a 2 + ⋯ + a i S_i=a_1+a_2+\cdots+a_i Si=a1+a2+⋯+ai。
数据范围
1
≤
Q
≤
1
0
6
,
∣
x
∣
≤
1
0
3
,
1
≤
k
≤
n
1\le Q\le10^6, |x|\le 10^3, 1\le k\le n
1≤Q≤106,∣x∣≤103,1≤k≤n
分析:
因为题目的操作都是对光标操作的,所以数据也被分成了光标左边的数据与光标右边的数据,那么维护中间值,在之前中位数那道题目中为了维护中位数,所以将中位数右边及中位数放在一个小根堆中,中位数左边的数放在一个大根堆中,这种做法也叫对顶堆。
而这道题目也是类似,不过使用的是对顶栈,使用两个栈 A A A 、 B B B,栈 A A A 维护的就是光标左边的数据,栈 B B B 维护的就是光标右边的数据。
对于 I x
操作:
- 把 x x x 插入栈 A A A ;
- 更新 s u m [ p A ] = s u m [ p A − 1 ] + A [ p A ] sum[p_A] = sum[p_A - 1] + A[p_A] sum[pA]=sum[pA−1]+A[pA] ;
- 更新 f [ p A ] = max ( f [ p A − 1 ] , s u m [ p A ] ) f[p_A] = \max(f[p_A - 1], sum[p_A]) f[pA]=max(f[pA−1],sum[pA]) ;
对于 D
操作,把
A
A
A 的栈顶出栈。
对于 L
操作,弹出
A
A
A 的栈顶,插入到
B
B
B 中。
对于 R
操作:
- 弹出 B B B 的栈顶,插入到 A A A 中;
- 更新 s u m [ p A ] = s u m [ p A ] + A [ p A ] sum[p_A] = sum[p_A] + A[p_A] sum[pA]=sum[pA]+A[pA] ;
- 更新 f [ p A ] = max ( f [ p A − 1 ] , s u m [ p A ] ) f[p_A]=\max(f[p_A-1],sum[p_A]) f[pA]=max(f[pA−1],sum[pA]) ;
对于 Q k
询问,直接返回
f
[
k
]
f[k]
f[k] 。
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 1;
int stkA[N], stkB[N];
int ttA = 0, ttB = 0;
int sum[N],f[N];
bool empty_A();
bool empty_B();
void push_A(int );
void push_B(int );
void pop_A();
void pop_B();
int top_A();
int top_B();
int main()
{
f[0] = - 0x3f3f3f3f;
int q;
scanf("%d", &q);
char op;
int x;
while(q--) {
cin >> op;
if(op == 'I') {
scanf("%d", &x);
push_A(x);
sum[ttA] = sum[ttA - 1] + x;
f[ttA] = max(f[ttA - 1], sum[ttA]);
} else if(op == 'Q') {
scanf("%d", &x);
printf("%d\n", f[x]);
} else if(op == 'D') {
if(empty_A())
pop_A();
} else if(op == 'L') {
if(empty_A()) {
x = top_A();
pop_A();
push_B(x);
}
} else {
if(empty_B()) {
x = top_B();
pop_B();
push_A(x);
sum[ttA] = sum[ttA - 1] + x;
f[ttA] = max(f[ttA - 1], sum[ttA]);
}
}
}
return 0;
}
bool empty_A() {
return ttA;
}
bool empty_B() {
return ttB;
}
void push_A(int x) {
stkA[++ttA] = x;
}
void push_B(int x) {
stkB[++ttB] = x;
}
void pop_A() {
ttA--;
}
void pop_B() {
ttB--;
}
int top_A() {
return stkA[ttA];
}
int top_B() {
return stkB[ttB];
}
【例题】火车进栈
这里有
n
n
n 列火车将要进站再出站,但是,每列火车只有
1
1
1 节,那就是车头。
这 n n n 列火车按 1 1 1 到 n n n 的顺序从东方左转进站,这个车站是南北方向的,它虽然无限长,只可惜是一个死胡同,而且站台只有一条股道,火车只能倒着从西方出去,而且每列火车必须进站,先进后出。
也就是说这个火车站其实就相当于一个栈,每次可以让右侧头火车进栈,或者让栈顶火车出站。
车站示意如图:
出站<—— <——进站
|车|
|站|
|__|
现在请你按《字典序》输出前
20
20
20 种可能的出栈方案。
数据范围
1
≤
n
≤
20
1\le n\le20
1≤n≤20
分析:
因为要输出方案,而且 n n n 不是很大,所以我们直接跑暴力就行了,对于每一个数我只有两种选择:
- 把下一个数进栈;
- 把当前栈顶的数出栈(如果栈非空)
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 21;
int n, p[N], pos = 1;
stack<int> stk;
int cnt = 0;
vector<vector<int>> ans;
void dfs(int u) {
if(pos > n) {
vector<int> temp;
for(int i = 1; i <= n; ++i) {
temp.push_back(p[i]);
}
ans.push_back(temp);
cnt++;
return ;
}
if(u > n) {
p[pos++] = stk.top();
stk.pop();
dfs(u + 1);
pos--;
stk.push(p[pos]);
return ;
}
if(stk.empty()) {
stk.push(u);
dfs(u + 1);
stk.pop();
}
else {
stk.push(u);
dfs(u + 1);
stk.pop();
p[pos++] = stk.top();
stk.pop();
dfs(u);
pos--;
stk.push(p[pos]);
}
}
int main()
{
scanf("%d", &n);
dfs(1);
sort(ans.begin(), ans.end());
for(int i = 0; i < 20 && i < ans.size(); ++i) {
vector<int> it = ans[i];
for(auto jt : it) {
cout << jt;
}
putchar('\n');
}
return 0;
}
进出栈序列问题
【例题】火车进出栈问题
一列火车 n n n 节车厢,依次编号为 1 , 2 , 3 , ⋯ , n 1,2,3,\cdots,n 1,2,3,⋯,n。
每节车厢有两种运动方式,进栈与出栈,问 n n n 节车厢出栈的可能排列方式有多少种。
数据范围
1
≤
n
≤
60000
1\le n\le 60000
1≤n≤60000
分析:
这个题目有 4 4 4 种解法,不过对于这个数据范围只有方法 4 4 4 解法能过。
方法一:搜索, 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,2,\cdots,N 1,2,⋯,N 时可能的进栈序列总数。然后把问题分解为若干个类似的子问题。
- 那么对于数字 1 1 1 的进栈顺序而言,它是第 1 1 1 个;
- 接着整数 2 2 2 ~ k k k 这 k − 1 k - 1 k−1 个数就会按照某种顺序进出栈;
- 然后数字 1 1 1 在第 k k k 个出栈;
- 后面的整数 k + 1 k + 1 k+1 ~ N N N 这 N − k N - k N−k 个数按某种顺序进出栈。
所以问题
S
N
S_N
SN 就被
1
1
1 这个数划分为 “
k
−
1
k - 1
k−1 个数进出栈” 和 “
N
−
k
N-k
N−k 个数进出栈” 这两个子问题。
S
N
=
∑
k
=
1
N
S
k
−
1
×
S
N
−
k
S_N = \sum_{k = 1}^{N} S_{k - 1}\times S_{N - k}
SN=k=1∑NSk−1×SN−k
方法三:动态规划, O ( n 2 ) O(n^2) O(n2)
动态规划第一步,定义状态,这个状态可以通过经验得出,也可以通过递归得出,设 F [ i , j ] F[i,j] F[i,j] 表示有 i i i 个数尚未进栈,目前有 j j j 个数在栈中,已经有 n − i − j n-i-j n−i−j 个数出栈时的方案总数,所以答案状态就是: F [ N , 0 ] F[N,0] F[N,0] 。
于是我通过递归方法里的状态转移不难得出对于 F [ i , j ] F[i,j] F[i,j] 是由决策 “把一个数进栈” 即 F [ i − 1 , j + 1 ] F[i-1,j+1] F[i−1,j+1] 和 “把栈顶弹出” 即 F [ i , j − 1 ] F[i,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] F[i,j]=F[i−1,j+1]+F[i,j−1]
方法四:数学, O ( N ) O(N) O(N)
如果把进出栈序列看作 01 01 01 串,那么很显然合法的进出栈序列的 01 01 01 串必须在任何时候 0 0 0 的个数大于或等于 1 1 1 的个数,而这个的问题又等价于求第 N N N 项的 C a t a l a n Catalan Catalan 数,即 C 2 N N n − 1 \frac{C_{2N}^{N}}{n-1} n−1C2NN 。不过这道题还需要高精度,且时间要求必须利用分解质因子来消除除法、使用万进栈加速高精度计算。
代码如下:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1010;
LL res[N], num[60001 * 2];
int len;
void prime(LL, LL);
void mul(LL);
void out();
int main()
{
res[0] = 1;
len = 1;
int n;
scanf("%d", &n);
for(int i = 1; i <= n; ++i) {
prime(n + i, 1);
}
for(int i = 1; i <= n + 1; ++i) {
prime(i, -1);
}
for(int i = 2; i <= 2 * n; ++i) {
//cout << i << ' ' << num[i] << endl;
for(int j = 0; j < num[i]; ++j)
mul(i);
}
out();
return 0;
}
void prime(LL x, LL d) {
for(int i = 2; i <= x/i && x != 1; ++i) {
while(x % i == 0) {
num[i] += d;
x /= i;
}
}
if(x) num[x] += d;
}
void mul(LL b) {
LL t = 0;
for(int i = 0;i < len; ++i) {
t += res[i] * b;
res[i] = t % 1000000000LL;
t /= 1000000000LL;
}
while(t) {
res[len++] = t % 1000000000LL;
t /= 1000000000LL;
}
}
void out() {
printf("%lld", res[len - 1]);
for(int i = len - 2; i >= 0; --i)
printf("%09lld", res[i]);
putchar('\n');
}
表达式计算
算术表达式分三种,中缀表达式、前缀表达式和后缀表达式。
我们平时数学计算使用的算术表达式就是中缀表达式。前缀表达式又称波兰式,后缀表达式又称逆波兰式。
后缀表达式求解和前缀表达式求解相同,利用递归就可以轻松解决,这种表达式也是机器喜欢的计算方式。
后缀表达式求值
- 建立一个用于存数的栈,逐个扫描该表达式中的元素。
(1) 如果遇到一个数,则把该数入栈。
(2) 如果遇到运算符,就取出栈顶的两个数进行计算,把结果入栈。 - 扫描完成后栈中恰好剩下一个数,就是该后缀表达式的值 。
对于中缀表达式处理却是最麻烦的,不过利用两个栈也可以扫一边解出表达式,分别是运算符栈、数字栈,确定运算符间的优先级。
中缀表达式求值
- 建立两个栈,一个数运算符栈,一个是数字栈。
(1)如果是数字,则把该数入数字栈。
(2)如果是左括号,则直接入运算符栈。
(3)如果是右括号,弹出运算符栈顶元素,直到为左括号。对于每个运算符进行运算,也就是从数字栈顶取两个数,然后运算结果在压入数字栈顶。
(4)如果是运算符,且运算符栈顶的优先级大于该运算符,就先把栈顶的运算符计算后再压入。 - 扫描完成后栈中恰好剩下一个数,就是该中缀表达式的值 。
代码如下:
#include<bits/stdc++.h>
using namespace std;
stack<int> num;
stack<char> op;
void eval() {
auto b = num.top(); num.pop();
auto a = num.top(); num.pop();
auto c = op.top(); op.pop();
int x;
if(c == '+') x = a + b;
else if(c == '-') x= a- b;
else if(c == '*') x = a * b;
else x = a / b;
num.push(x);
}
int main() {
unordered_map<char, int> pr{{'+', 1}, {'*', 2}, {'/', 2}};
string str;cin >> str;
for(int i = 0; i < str.size(); ++i){
auto c = str[i];
if(isdigit(c)) {
int x = 0, j = i;
while(j < str.size() && isdigit(str[j])) x = x * 10 + str[j++] - '0';
i = j - 1;
num.push(x);
} else if(c == '(') {
op.push(c);
} else if(c == ')'){
while(op.top() != '(') eval();
op.pop();
} else {
while(!op.empty() && pr[op.top()] >= pr[c]) eval();
op.push(c);
}
}
while(op.size()) eval();
cout << num.top() << endl;
return 0;
}
单调栈
【例题】直方图中最大的矩形
直方图是由在公共基线处对齐的一系列矩形组成的多边形。
矩形具有相等的宽度,但可以具有不同的高度。
例如,图例左侧显示了由高度为
2
,
1
,
4
,
5
,
1
,
3
,
3
2,1,4,5,1,3,3
2,1,4,5,1,3,3 的矩形组成的直方图,矩形的宽度都为
1
1
1:
通常,直方图用于表示离散分布,例如,文本中字符的频率。
现在,请你计算在公共基线处对齐的直方图中最大矩形的面积。
图例右图显示了所描绘直方图的最大对齐矩形。
数据范围
1
≤
n
≤
100000
,
0
≤
h
i
≤
1000000000
1\le n\le 100000 ,~ 0\le h_i\le1000000000
1≤n≤100000, 0≤hi≤1000000000
分析:
对于图中高度不一的柱子而言,如果我们只看某一柱子的高度作为矩阵的高度,那么他的宽度则最多延伸到左边第一个小于它的高度的柱子,和右边第一个小于它的高度的柱子。
那么如果我们知道了所有柱子左边第一个小于它的高度的柱子位置和右边第一个小于它的高度的柱子位置,那么在跑一个循环就可以得出答案了。
所以问题就转化为了,如果快速的计算每个位置左边第一个小于它高度的柱子的位置。
代码如下:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 100003;
int n, lans[N], rans[N];
LL a[N];
int stk[N], tt = 0;
void get_ans(int ans[]) {
tt = 0;
a[0] = -1;
for(int i = 1; i <= n; ++i) {
while(a[stk[tt]] >= a[i]) tt--;
ans[i] = stk[tt];
stk[++tt] = i;
}
}
int main()
{
while(scanf("%d", &n) && n) {
for(int i = 1; i <= n; ++i)
scanf("%lld", &a[i]);
get_ans(lans);
reverse(a + 1, a + 1 + n);
get_ans(rans);
LL res = 0;
for(int i = 1; i <= n; ++i) {
res = max(res, (n + 1 - lans[n - i + 1] - rans[i] - 1) * a[i]);
}
printf("%lld\n", res);
}
return 0;
}