算法竞赛进阶指南 基本数据结构 0x11 栈

栈 是实现机器递归(深度优先搜索)的基本结构

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=1NSk1SNk(注意是相乘)

思路三:动态规划 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[i1,j+1]+F[i,j1]

思路四:数学 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);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值