栈是一种在竞赛中非常实用的基础数据结构。本篇笔记主要记录了栈的一些例题与技巧。
基本性质
对于一个栈,保证有以下性质
- 先入栈的元素一定后出
- 后入栈则反之
例如对于$3,6,0,1,2$这一组数据来说,假设我们将他们全部入栈,再全部出栈,那么出栈后的序列为$2,1,0,6,3.$
【例题】$push,pop,Getmin$
实现一个栈,支持入栈,出栈和查询最小值的工作,要求时间复杂度均为$O(1).$
$ $对于入栈、出栈的功能,$c++ STL stack$中自带的函数时间复杂度为$O(1)$。但是对于查询最小值而言,栈本身的性质并不支持这种操作。
$ $我们可以考虑在维护一个$stack$的同时,再维护一个二叉堆(即优先队列),这样就可以达到题目的要求。然而,这样的时间复杂度为$O(logN).$
$ $所以我们需要一种更高效的算法。我们发现,每一次入栈、出栈的时间复杂度均为$O(1)$;而我们又知道,假设有两个数$x,y$,当我们每对这两个数做出一次比较是,时间复杂度也是$O(1).$我们利用这样的性质,同时维护两个栈结构$P_1,P_2$,其中$P_1$存放的是原本的数据,而$P_2$存放的是从$P_1$栈底$P_{1_{0/1}}$至当前栈顶$P_{1_n}$的最大值。
$ $这样,我们就可以梳理出一个程序的脉络$.$
- 若此次执行的是$push$操作,则将此元素$x$压进$P_1.$并将$x$与$P_{2_n}$比较,若$x$小于$P_{2_n}$,则将$x$压入$P_2.$
- 若此次执行的是$pop$操作,则将当前$P_{1_n},P_{2_n}$全部弹出$.$
- 若此次执行的是$Getmin$操作,则直接返回$P_{2_n}$即可$.$
部分代码:
if(s=="push") { P1.push(x); if(x<P2.top()) P2.top(x); } else if(s=="Pop") { P1.pop(); P2.pop(); } else { unsigned long long f=P2.top(); cout<<f<<endl; }
$ $在上面的例题中,我们巧妙地利用了栈的性质,通过维护两个栈达到了目的。我们通过利用栈,还可以再很乐观的时间复杂度里做出一道题目,甚至可以与$dp$(动态规划)比拟!
又例如说$noip$一道经典的例题:“栈”
关于$noip,it's dead$
不!今天不讨论这个!
题意简述:
给出$n$个数字以及一个深度大于$n$的栈,每次可以执行入栈/出栈$2$种操作。问可以得到多少种不同的出栈序列$.$
$ $我们可以思考出一种很显然的暴力搜索:对于每次操作$d$我们只有两种选择
- 将当前的栈定元素出栈
- 将当前未经栈序列中的一个数进栈
$ $使用这样类似于$dfs$的思想,我们可以正确的做出这题。然而,这样需要枚举$n$个数,所以时间复杂度约为$O(2^n)$,我们肯定是不能接受的$.$
$ $所以我们考虑如何降低时间复杂度。上述算法的时间主要来源是枚举$n$个数的开销。我们可以想到题目只是让我们求出种数,而并不关心每种方案的内容。根据这种思想,我们可以发现一个很显然的不断求子问题的递推方案。假设方案总数为$f_n$。
$ $以$1$为例子来说,假设$1$放在第$j$位出栈,那么这个问题就被划分成了$2$个子问题:
- 前$j-1$个数模拟进出栈的过程
- 后$n-j$个数模拟进出栈的过程
然后按照这样的过程递推下去,可以得到递推式:
$$f_n=\sum\limits_{j=1}^nf_{j-1}*f_{n-j}$$
$ $到这里,递推的$O(n^2)$复杂度下就可以$\mathtt{AC}$了。但是本题还有一种数学方法,也就是$Catalan$数。具体可以参见$Catalan$数资料$.$
后缀表达式
后缀表达式是栈中一个很重要的分支,我们把它抓出来单独讲
表达式的概念
$ $众所周知,我们在平常的数学中试使用的使这样的式子:
- $3*(2-1)$
$ $像这样,运算符号在运算数字中间的叫做中缀表达式$.$
根据中缀表达式的写法,又拓展出
- $* 3 - 2 1 $
$ $像这样,运算符号在运算数字前面的叫做前缀表达式$.$
$ $同样可以拓展出
- $ 2 1 - 3 *$
$ $像这样,运算符号在运算数字后面的叫做后缀表达式$.$
在这里,我们不再叙述各种表达式的算法,而是直接把目光放到后缀表达式在$c++$中的写法上来。
$ $我们首先考虑$dfs$的做法。在录入符号时,就返回运算的值。但这样程序复杂度高,且细节不容易处理,更别说时时间复杂度了,一般都是指数级别的。
$ $所以我们思考是否有更好的做法。记得$lxl$曾说数据结构可以让程序跑的更快,所以我们思考是否可以用数据结构解决这题。仔细研究后我们发现,后缀表达式始终满足符号最后录入这个条件,这也就意味着我们可以在线询问。于是,我们想到了一种使用栈的方法$.$
$ $我们设一个栈$P$,并遍历后缀表达式,如果是数字就入栈,如果不是就取出当前栈顶的两个元素进行运算。注意,这里要把结果入栈。当遍历完后,栈顶的元素就是答案。
下面是一些常见的问题:
$Q:$运算符号不用入栈么?
$A:$不需要。遇到运算符号计算就好,没必要入栈。
$Q:$为什么计算出来的结果要入栈?
$A:$因为后缀表达式是没有括号的,所以计算优先级一定,计算出来的结果下一步还要用。
栈的一些小技巧
手写栈嫌麻烦?使用$C++ STL stack!$
$c++$的标准模板库$(STL)$中自带栈$(stack)$的函数。支持单调栈以及栈的所有基本操作(还包括您不知道的)!
栈为什么快?
- 栈是一种线性结构
- 栈的基本操作时间复杂度均为$O(1)$