栈是个很重要且可以实现很多操作的数据结构,这种数据结构我分为几个部分介绍,有普通栈、对顶栈、单调栈、用栈实现表达式计算。
栈的原理
栈是一种"后进先出“的线性数据结构。栈只有一段能够进元素,我们一般称这一段为栈顶,另一端为栈底。添加或删除栈中元素时,我们只能将其插入到栈顶(进栈),或者把栈顶元素从栈中取出(出栈)
栈主要分为两个操作,用数组是这样实现的,用top表示当前所指的栈顶
入栈操作即把数组stk[top]赋值,然后top自增
出栈的操作便是top--
这便实现了简单的栈的操作,STL中有stack<int> 定义然后pop出栈,push则入栈
单调栈
说实话我对单调栈的理解并不是很深刻但我需要再加深印象,单调栈顾名思义,便是单调递减或单调递增的,可以用维护单调递减实现很多操作
例题1:https://www.acwing.com/problem/content/90/
这道题虽然前面两个操作用一个栈来进行操作即可,但是每次取出最小值需要用一个单调栈去快速取出,可以建立两个栈,A存本来的数据,B则取到当前top的历史的最小值,我们很容易知道维护其栈顶的方式便是插入min(B的栈顶数据,x),这便是到目前的最小值,在进行pop操作时A、B都从栈顶弹出,getmin便是取出B的栈顶即可
代码如下
class MinStack {
public:
/** initialize your data structure here. */
stack<int> a,b;
MinStack() {
}
void push(int x) {
a.push(x);
if(b.size()) x=min(b.top(),x);
b.push(x);
}
void pop() {
a.pop();
b.pop();
}
int top() {
return a.top();
}
int getMin() {
return b.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();
*/
这道题可谓是非常的经典,可以在O(N)的时间复杂度的方法及时删除不可能的选项,保持测率集合的高度有效性和秩序性,而为决策提供更多的条件和可能方法
思路大概是这样的
倘若想象矩形的高度是从左到右递增的,那答案便是尝试把每个矩形的高度作为最终矩形的高度去向右边延伸,宽度延长到边界便是可以用这个高度的矩形,此时取这个矩形去和res比较取出最大值便可
有了这样的特殊条件我们可以像我们是否能够推广到一般情况, 那边是出现了不是升序的时候便操作前面的矩形,然后进行出栈操作维护一个递增的序列,这样便可以得到,因为每个只会入栈一次所以时间复杂度只有N
代码如下
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1000005;
long long stk[N],b[N],a[N],n,top=0;
int main(){
while(cin>>n){
memset(b,0,sizeof b);
memset(stk,0,sizeof stk);
if(n==0) break;
long long maxn=0;
for(int i=1;i<=n;i++){
cin>>a[i];
}
a[n+1]=0;
for(int i=1;i<=n+1;i++){
if(stk[top]<a[i]||top==0){
stk[++top]=a[i];
b[top]=i;
}
else if(stk[top]>a[i]){
long long s=1e9;
while(stk[top]>a[i]){
s=min(s,b[top]);
maxn=max(maxn,(i-b[top])*stk[top]);
b[top]=0;
stk[top]=0;
top--;
}
stk[++top]=a[i];
b[top]=s;
}
}
cout<<maxn<<endl;
}
}
洛谷的地址是P4147 玉蟾宫 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这道题看起来很难一样但分析便可知每一次获取直方图信息便能解决这个水题
代码如下
#include<iostream>
#include<cstring>
using namespace std;
int a[2000][2000];
long long stk[40000],top,b[40000];
long long res=0;
int n,m;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){//预处理获得每一层的直方图数据
for(int j=1;j<=m;j++){
char c;
cin>>c;
if(c=='R') a[i][j]=0;
else a[i][j]=a[i-1][j]+1;
}
}
/* for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cout<<a[i][j]<<" ";
}
cout<<endl;
}*/
for(int i=1;i<=n;i++){//进行栈的操作
a[i][m+1]=0;
top=0;
memset(stk,0,sizeof stk);
memset(b,0,sizeof b);
for(int j=1;j<=m+1;j++){
if(a[i][j]>stk[top]|| top==0){
stk[++top]=a[i][j];
b[top]=j;
}
else if(a[i][j]<stk[top]){
long long width=1e9;
while(stk[top]>a[i][j]){
width=min(width,b[top]);
// cout<<j<<" "<<b[top]<<" "<<stk[top]<<endl;
res=max(res,(j-b[top])*stk[top]);
top--;
}
stk[++top]=a[i][j];
b[top]=width;
}
}
}
cout<<res*3<<endl;
}
表达式计算
1.后缀表达式 链接P1449 后缀表达式 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
后缀表达式便是可以用计算机实现的,可以这样,用两个栈,一个数字栈,一个运算符栈,倘若有运算符便从数字栈取出两个数进行计算,把结果入栈百年可,扫描完成后,栈最后只会剩下一个数,这便是后缀表达式的值
代码是这样的
#include<iostream>
using namespace std;
int s[1000],top=0;
int main(){
char a;
int ans=0;
while(cin>>a){
if(a=='@'){
break;
}
else if(a>='0'&&a<='9'){
ans=ans*10+a-'0';
}
else if(a=='.'){
s[++top]=ans;
ans=0;
}
else if(a=='-'){
int w=s[top-1]-s[top];
s[top]=0;
top-=1;
s[top]=w;
}
else if(a=='*'){
int w=s[top-1]*s[top];
s[top]=0;
top-=1;
s[top]=w;
}
else if(a=='/'){
int w=s[top-1]/s[top];
s[top]=0;
top-=1;
s[top]=w;
}
else if(a=='+'){
int w=s[top-1]+s[top];
s[top]=0;
top-=1;
s[top]=w;
}
}
cout<<s[top];
}
2.中缀表达式AcWing 151. 表达式计算4 - AcWing
这里就必须要用优先级来实现,倘若优先级不呈现递增便出栈直到实现递增,然后要左括号直接进,右括号便可以一直出栈直到得到结果
代码如下
#include<iostream>
#include<stack>
#include<cmath>
#include<unordered_map>
using namespace std;
typedef long long ll;
stack<ll> sum;
stack<char> op;
unordered_map<char,int> h={
{'+',1},{'-',1},{'*',2},{'/',2},{'^',3},{'(',0}
};
void f(){
ll a=sum.top();sum.pop();
ll b=sum.top();sum.pop();
char c=op.top();op.pop();
ll k=0;
if(c=='+') k=a+b;
if(c=='-') k=b-a;
if(c=='*') k=a*b;
if(c=='/') k=b/a;
if(c=='^') k=pow(b,a);
sum.push(k);
}
int main(){
string str,t;
cin>>str;
while(t.size()!=str.size()) t+="(";
str=t+str+")";
int len=str.size();
int flag=0;
int i=0;
/*if(str[i]=='-'){
flag=1;
i++;
}*/
for(;i<len;i++){
if(str[i]=='-'&&str[i-1]!=')'&&!isdigit(str[i-1])){
sum.push(0);
}
if(isdigit(str[i])){
int j=i;
ll g=0;
while(isdigit(str[j])&&j<len){
g=g*10+str[j]-'0';
j++;
}
sum.push(g);
i=j-1;
}
else if(str[i]=='('){
op.push(str[i]);
}
else if(str[i]==')'){
while(op.top()!='('){
f();
}
op.pop();
}
else{
while(op.size()&&h[str[i]]<=h[op.top()]) f();
op.push(str[i]);
}
}
while(op.size()){
char c=op.top();
if(c=='+'||c=='-'||c=='*'||c=='/') {
f();
}
op.pop();
}
cout<<sum.top()<<endl;
}
对顶栈
对顶栈操作和对顶堆很像就是在中间位置以光标位置为栈顶,向左便左出右进,向右便右出左进
求和就用前缀和即可
代码如下
{
int Q;
cin>>Q;
f[0]=-1e7;
while(Q--){
char a;
cin>>a;
if(a=='I'){
int b;
cin>>b;
stk1[++top1]=b;
sum[top1]=sum[top1-1]+b;
f[top1]=max(f[top1-1],sum[top1]);
}
if(a=='D'&&top1){
top1--;
}
if(a=='L'&&top1){
stk2[++top2]=stk1[top1--];
}
if(a=='R'&&top2){
stk1[++top1]=stk2[top2--];
sum[top1]=sum[top1-1]+stk1[top1];
f[top1]=max(f[top1-1],sum[top1]);
}
if(a=='Q'){
int b;
cin>>b;
cout<<f[b]<<endl;
}
}
}
双栈与二分图的关系
例题 P1155 [NOIP2008 提高组] 双栈排序 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
acwing的网址AcWing 153. 双栈排序 - AcWing
当用两个栈来进行操作的时候我们就可以把两个栈分别当做是二分图的左端和右端,用一种状态表示两者i和j不能在同一个栈中,用染色法便可以把他们分到不同的栈中然后进行模拟操作
该题的题意很简单,便是用两个栈来实现排序
本题可以这样分析,先分析用一个栈的情况,便是要当后面没有小于当前栈顶的数的时候才能进行出栈操作,这一点很重要
当有两个栈时便复杂了一些,
但我们可以这样考虑当确定了第一个栈的时候其出队入队的顺序都能够确定同时第二个栈的也能够以此确定便可以用模拟完成
这里要证明到一种性质,才能使左端和右端有条件限制
这个性质便是当且仅当i<j<k,a[k]<a[i]<a[j],i和j不能在同一个栈中
如何去证明这个性质呢,
正确性
假设i和j可以入同一个栈,那在j进栈之前i便必须要出栈(因为i的值更小),但此时的i也不能出栈要不然a[k]更小便会产生逆序对,这便是对的
充分性
如何去证明这个性质的充分性呢,这可以考虑到其他情况
倘若i<j,此时a[i]<a[j]在a[i]入栈后便可以出栈,唯一能够限制其的条件就是在后面还有比a[i]更加小的数,不然a[i]出栈后a[j]再入栈此时还是在同一个栈里面
这便是这个性质的充分性
这样我们便找到了i和j分别在二分图左右两端的条件,以这个条件建二分图,
我使用的是记录节点i以后的最小值,这便是k,然后去枚举,倘若出现了上述这般条件,便把i和j建边,再用染色法判定二分图倘若是二分图再进行下一步操作,倘若不是那就不行
做完二分图操作后就有一点很麻烦的便是模拟栈,y总的思路都不是完全正确的,首先要按照优先级字典序来操作,那便是push(1)<pop(1)<push(2)<pop(2),我们可以去模拟2*n次,能做第一个操作就做第一个,不然能做第二个就做第二个,再不然能做第三个便做第三个,最后再考虑第四个,这样便是不会出错的解法
代码如下
#include<iostream>
#include<stack>
#include<cstring>
using namespace std;
const int N=2000;
int n;
int a[N],b[N][N],f[N];//a表示原数数组,b[i][j]表示i和j的联通关系,f[i]表示i之后的最小数
int color[N];//颜色
int dfs(int k,int c){//染色法判断二分图
color[k]=c;
for(int i=1;i<=n;i++){
if(b[k][i]){
if(color[i]==c) return false;
if(color[i]==-1 && !dfs(i,!c)){
return false;
}
}
}
return true;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
f[n+1]=1e9;
memset(b,0,sizeof b);
for(int i=n;i>=1;i--) f[i]=min(a[i],f[i+1]);
for(int i=1;i<=n;i++){//建边
for(int j=i+1;j<=n;j++){
if(a[i]<a[j]&&f[j+1]<a[i]){
b[i][j]=1;
b[j][i]=1;
}
}
}
memset(color,-1,sizeof color);
int flag=1;
for(int i=1;i<=n;i++){
if(color[i]==-1){
if(!dfs(i,0)){
flag=0;
break;
}
}
}
if(flag==0){//不是二分图说明要第三个栈了不满足题意
cout<<0<<endl;
return 0;
}
stack<int> s1,s2;//栈一和栈二
int now=1;//出栈顺序
int j=1;//j才是滑动的指针,i只是模拟的次数
for(int i=1;i<=2*n;i++){//贪心策略是能a就a能b就b这样就是更优的,模拟栈进出n次即可
if(j<=n&&color[j]==0 && (!s1.size() || s1.size() && a[j]<s1.top())){
s1.push(a[j]);
cout<<"a ";
j++;
}
else if(s1.size()&& now==s1.top()){
cout<<"b ";
s1.pop();
now++;
}
else if(j<=n&&color[j]==1&&(!s2.size()||s2.size()&&a[j]<s2.top())){
s2.push(a[j]);
cout<<"c ";
j++;
}
else if(s2.size()&&now==s2.top()){
cout<<"d ";
s2.pop();
now++;
}
}
}