栈的经典运用

本文介绍了对顶栈在AcWing题目中的应用,包括火车进栈和火车进出栈问题的解决方案。通过 dfs 和01序列转换,解决不同场景下的问题,并探讨了如何优化算法以达到O(1)的复杂度。同时,提到了直方图中最大矩形、城市游戏的二维单调栈解法以及括号序列和中缀表达式的计算问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

AcWing 128.编辑器
话说这题要拿80还是挺简单的,我居然都可以想到对顶栈的办法。
本题参考动态中位数的对顶堆思想,很容易想到以光标为分界线,将已有的序列划分别划入两个栈中。我们不妨将这种方法命名为“对顶栈”。
前面4个操作的实现相当简单,但是最后一个略显困难。当然,我们很容易想到一种暴力枚举的方法如下:

	while(Q--){
      	char c;
       	cin>>c;
        if(c=='I'){
            int x;
            cin>>x;
            stk2[++top2]=x;
        }
        if(c=='D'){
            if(top2){
                top2--;
            }
        }
        if(c=='L'){
            if(top2){
                stk1[++top1]=stk2[top2];
                top2--;
            }
        }
        if(c=='R'){
            if(top1){
                stk2[++top2]=stk1[top1];
                top1--;
            }
        }
        if(c=='Q'){
            int x;
            cin>>x;
            int ans=0,zf=0;
            for(int i=1;i<=min(top2,x);i++){
                if(stk2[i]>0 || i==1){
                    if(zf+stk2[i]>0 || i==1){
                        ans+=zf+stk2[i];
                        zf=0;
                    }
                    else zf+=stk2[i];
                }
                else{
                    zf+=stk2[i];
                }
            }
            cout<<ans<<endl;
        }
    }

这种方法当然不够优秀,只能拿到80pts,所以我们要尝试优化,将所有操作的复杂度提升至O(1)。那么,通过思考可知:新加入一个数以及向右移动光标时需要更新答案。我们不妨用数组f[i]来存储答案,询问时直接输出f[k]即可。而f数组的求法也很简单,见代码。

 		if(c=='I'){
            int x;
            scanf("%d",&x);
            stk2[++top2]=x;
            if(x+sum[top2-1]-f[top2-1]>0 || top2==1){
                f[top2]=x+sum[top2-1];
            }
            else f[top2]=f[top2-1];
            sum[top2]=x+sum[top2-1];//sum是前缀和数组
        }
        if(c=='R'){//两处代码完全相同
            if(top1){
                int x=stk1[top1];
                stk2[++top2]=x;
                top1--;
                if(x+sum[top2-1]-f[top2-1]>0 || top2==1){
                    f[top2]=x+sum[top2-1];
                }
                else f[top2]=f[top2-1];
                sum[top2]=x+sum[top2-1];
            }
        }

总结:“对顶”数据结构适合维护一个有序的序列,支持删除、加入、在线询问等操作。要视具体情况正确使用不同的结构。

1.AcWing 129.火车进栈

本题数据范围较小,又是要输出方案,必然想到用dfs。那么此处的dfs较为特殊,是每次有两种选择的dfs(每次可以选择将栈顶的数出栈或者是将下一个数进栈),那么代码就应当写成每次递归只进行一次操作的样子,而不是循环多次操作。代码如下:

#include<iostream>
using namespace std;
const int N=25;
int stack[N],cnt,top,ans[N],m;
int n;
void dfs(int x){
    if(x==n+1){
        cnt++;
        if(cnt>20) return;
        for(int i=1;i<=m;i++) cout<<ans[i];
        for(int i=top;i>=1;i--) cout<<stack[i];
        cout<<endl;
        return;
    }
    if(top){
        ans[++m]=stack[top--];
        dfs(x);
        stack[++top]=ans[m--];
    }
    stack[++top]=x;
    dfs(x+1);
    --top;
}
int main(){
    cin>>n;
    dfs(1);
    return 0;
}

2.AcWing 130.火车进出栈问题

这个问题并不要求具体方案,而是要求方案数量。那么考虑将进栈操作视为0,出栈操作视为1,将整个出栈序列变成01序列。那么这个序列有一个很明显的特点就是:该序列的任何一个前缀中0的个数均大于等于1的个数(因为不论什么时候,进栈的次数都要不少于出栈的次数)。那么这个问题就转化为了满足条件的01序列,可以直接通过求卡特兰数来解决。
但是这个题目很ex,不但要用高精度,而且要用优化后压位的高精度。
这个题目真应该放到数学里
我们考虑通过把答案所有的质因数直接相乘起来的办法来提高求解的效率。那么这里又用到了一个模板题阶乘分解,这个模板可以将n!以logn的时间复杂度求出n!中质因数p的个数。

代码(未压位)如下:

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N=6e4+5;
int prime[N],cnt,index[N];
bool st[N];
void get_prime(int n){
    for(int i=2;i<=n;i++){
        if(!st[i]) prime[++cnt]=i;
        for(int j=1;prime[j]*i<=n;j++){
            st[prime[j]*i]=true;
            if(i%prime[j]==0) break;
        }
    }
}
int get(int n,int p){
    int ans=0;
    while(n){
        ans+=n/p;
        n/=p;
    }
    return ans;
}
vector<int> multi(vector<int> a,int b){
    vector<int> c;
    int t=0;
    for(int i=a.size()-1;i>=0 || t;i--){
        if(i>=0) t+=a[i]*b;
        c.push_back(t%10);
        t/=10;
    }
    while(c.size()>1 && c.back()==0) c.pop_back();
    reverse(c.begin(),c.end());
    return c;
}
int main(){
    int n;
    cin>>n;
    get_prime(2*n);
    for(int i=1;i<=cnt;i++){
        int p=prime[i];
        index[i]=get(2*n,p)-2*get(n,p);
    }
    int k=n+1;
    for(int i=1;prime[i]<=k && i<=cnt;i++){//将n+1中的质因子个数除去
        while(k%prime[i]==0){
            k/=prime[i];
            index[i]--;
        }
    }
    vector<int> res;
    res.push_back(1);
    for(int i=1;i<=cnt;i++){
        for(int j=1;j<=index[i];j++){
            res=multi(res,prime[i]);
        }
    }
    for(int i=0;i<res.size();i++) cout<<res[i];
    return 0;
}

总结:这个题目还是一个很好的数学题:用到了组合计数中的Catlan数、质数线性筛、阶乘质因数分解、普通质因数分解等技巧。
1. AcWing 131.直方图中最大的矩形
思路的话没什么好说的,就是要记住这种处理思想。每次进来一个数,都将栈中比自己高的矩形全部出栈并当场计算其面积(因为一旦自己加入了,前面比自己高的矩形不能再扩张了,只能当即算出来)。如此反复后就可以求出最大值了。
代码如下:

#include<iostream>
using namespace std;
const int N=1e5+5;
typedef long long ll;
struct Node{
    ll h,s;
}stk[N];
int top;
ll h[N];
int main(){
    int n;
    while(cin>>n && n){
        ll ans=0;
        for(int i=1;i<=n;i++){
            cin>>h[i];
        }
        h[n+1]=0;//这个是为了防止所有处理完后栈中还有剩余的矩形
        for(int i=1;i<=n+1;i++){
            int S=0;
            while(top && h[i]<=stk[top].h){
                S+=stk[top].s;
                ans=max(ans,S*stk[top].h);
                top--;
            }
            stk[++top]={h[i],S+1};
        }
        cout<<ans<<endl;
    }
    return 0;
}


2.AcWing 152.城市游戏(二维单调栈)
这题乍看起来很难做,但我们观察一下样例并自己构造数据后发现这枚举矩形的过程像极了那道单调栈的题目直方图中最大的矩形。所以我们可以想办法把这个矩阵转化为直方图,方法就很简单了:直接枚举每一行作为直方图的底部,然后预处理出每一行对应每一列上方连续的F的个数并直接代入单调栈模板进行计算。时间复杂度为O(NM)。代码如下:

#include<iostream>
using namespace std;
const int N=1005;
char a[N][N];
int h[N][N];//表示点(i,j)上面(包括自身)有多少个连续的F
struct Node{
    int h,s;
}stk[N];
int top;
int main(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++) cin>>a[i][j];
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(a[i][j]=='F'){
                h[i][j]=h[i-1][j]+1;
            }
            else h[i][j]=0;
        }
    }
    int ans=0;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m+1;j++){
            int s=0;
            while(stk[top].h>h[i][j]){
                s+=stk[top].s;
                ans=max(ans,s*stk[top].h);
                top--;
            }
            stk[++top]={h[i][j],s+1};
        }
        top=0;
    }
    cout<<ans*3<<endl;
    return 0;
}

​1.AcWing 150.括号画家(括号序列类问题)

这题没什么可说的,就是将左括号压入栈内,右括号一个个解锁弹出左括号。弹出的连续序列就是美观的括号序列。但是不知道为什么我自己的代码写挂了,下面是AC的标程:

#include<iostream>
#include<cstring>
using namespace std;
const int N=1e5+5;
char s[N];
int stk[N],top;
int main(){
    cin>>s+1;
    int n=strlen(s+1);
    int res=0;
    for(int i=1;i<=n;i++){
        if(top){
            char c=s[stk[top]];
            if(c=='(' && s[i]==')' || c=='[' && s[i]==']' || c=='{' && s[i]=='}'){
                top--;
            }
            else stk[++top]=i;
        }
        else{
            stk[++top]=i;
        }
        if(top) res=max(res,i-stk[top]);
        else res=max(res,i);
    }
    cout<<res;
    return 0;
}

2.AcWing 151.表达式计算4(中缀表达式,顺带要复习前缀与后缀表达式)
这题及其毒瘤,要考虑的情况特别多,难以修改,自己写的只能拿50pts。这题可以了解一下:具体的计算方法见《算法进阶指南》P52-53。然后还有一些奇奇怪怪的细节问题。

#include<iostream>
#include<cmath>
#include<stack>
using namespace std;
stack<char> op;
stack<int> num;
void calc(){
    int a=num.top();num.pop();
    int b=num.top();num.pop();
    int d=0;
    if(op.top()=='+'){
        d=a+b;
    }
    if(op.top()=='-'){
        d=b-a;
    }
    if(op.top()=='*'){
        d=b*a;
    }
    if(op.top()=='/'){
        d=b/a;
    }
    if(op.top()=='^'){
        d=pow(b,a);
    }
    num.push(d);
    op.pop();
}
int main(){
    string s,left;
    cin>>s;
    if(s[0]=='-') s='0'+s;
    for(int i=0;i<s.size();i++){//存一堆左括号,防止右括号多余
        left+='(';
    }
    s=left+s+')';
    for(int i=0;i<s.size();i++){
        if(s[i]>='0' && s[i]<='9')
        {
            int j=i,t=0;
            while(s[j]>='0' && s[j]<='9'){
                t=t*10+s[j]-'0';
                j++;
            }
            i=j-1;
            num.push(t);
        }
        else if(s[i]=='-' || s[i]=='+')
        {
            if(s[i]=='-' && i && !(s[i-1]>='0' && s[i-1]<='9') && s[i-1]!=')'){
                if(s[i+1]=='('){
                    op.push('*');
                    num.push(-1);
                }
                else{
                    int j=i+1,t=0;
                    while(s[j]>='0' && s[j]<='9'){
                        t=t*10+s[j]-'0';
                        j++;
                    }
                    i=j-1;
                    num.push(-t);
                }
            }
            else{
                while(op.top()!='(') calc();
                op.push(s[i]);
            }
        }
        else if(s[i]=='*' || s[i]=='/')
        {
            while(op.top()=='*' || op.top()=='/' || op.top()=='^') calc();
            op.push(s[i]);
        }
        else if(s[i]=='^')
        {
            while(op.top()=='^') calc();
            op.push(s[i]);
        }
        else if(s[i]==')')
        {
            while(op.top()!='(') calc();
            op.pop();
        }
        else
        {
            op.push(s[i]);
        }
    }
    while(op.size()){
        if(op.top()=='(') op.pop();
        else calc();
    }
    cout<<num.top();
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值