解题报告——双栈排序

本文探讨了一种使用两个栈进行升序排序的问题,详细分析了如何通过特定操作序列实现排序。对于给定的1-n排列,文章讨论了如何判断是否能通过双栈排序,以及如何找到字典序最小的操作序列。作者提到了从特殊到一般的思考方法,从单栈到双栈的转换,并指出问题可以转化为二分图问题,从而在n^2的时间复杂度内解决。文章附带了解决此问题的代码实现。

题目(转自洛谷

Tom最近在研究一个有趣的排序问题。如图所示,通过2个栈S1和S2 ,Tom希望借助以下4种操作实现将输入序列升序排序

操作a
如果输入序列不为空,将第一个元素压入栈S1
操作b
如果栈S1空,将S1栈顶元素弹出至输出序列
操作c
如果输入序列不为空,将第一个元素压入栈S 2
操作d
如果栈S2不为空,将S2栈顶元素弹出至输出序列

如果一个1−n的排列P可以通过一系列操作使得输出序列为1,2,…,(n-1),n1,2,…,(n−1),n,Tom就称PP是一个“可双栈排序排列”。例如(1,3,2,4)(1,3,2,4)就是一个“可双栈排序序列”,而(2,3,4,1)(2,3,4,1)不是。下图描述了一个将(1,3,2,4)(1,3,2,4)排序的操作序列:<a,c,c,b,a,d,d,b><a,c,c,b,a,d,d,b>

当然,这样的操作序列有可能有几个,对于上例(1,3,2,4)(1,3,2,4),<a,c,c,b,a,d,d,b><a,c,c,b,a,d,d,b>是另外一个可行的操作序列。Tom希望知道其中字典序最小的操作序列是什么。

输入格式
第一行是一个整数n。

第二行有n个用空格隔开的正整数,构成一个1−n的排列。

输出格式
共一行,如果输入的排列不是“可双栈排序排列”,输出数字00;否则输出字典序最小的操作序列,每两个操作之间用空格隔开,行尾没有空格。

输入输出样例
输入 #1
4
1 3 2 4
输出 #1
a b a a b b a b
输入 #2
4
2 3 4 1
输出 #2
0
输入 #3
3
2 3 1
输出 #3
a c a b b d
说明/提示
30%的数据满足: n≤10
50%的数据满足:n≤50
100%的数据满足:n≤1000

观察数据规模,发现这道题对时间复杂度的要求并不高,n2的算法可以轻松通过,甚至连logn n2也是可以的,再观察题干,发现这道题如果跳过模拟直接得出答案很困难,凭一己之力很难找到规律,所以最开始还是要从模拟当中找到答案。但是直接模拟也很难。让人有一种无从下手的感觉,那我们还是从特殊到一般试试。看见“双”,先想“单”,假设我们只有一个栈,怎么排序?在排序中,栈的作用到底是什么?仔细想一想,不难发现其实就是辅助交换逆序对,并且它的能力有限,对于2 3 1这样的数据是无能为力的。
那么“2 3 1”,这种数据有什么性质?它不单调!,对于没有单调性质的数据。我们会发现都不可以用单栈排序。
那么扩展到双栈呢?,这种情况下,我们就可以将1压进1号栈,2压进2号栈,3压进1号栈,再分别弹出3,1,2,就完成了排序。
那当其中有一个栈失去了单调性质,那么这个排序也就gg了。所以说,我们要判断可不可以用双栈排序,本质上就是要判断数列中存不存在a4<a1<a2<a3这一类的数据。当然我们可以暴力判O(4^n) , 也可以优化一下先预处理出后缀最小值,然后O(n^3 )判断,当然都会TLE。
那怎么办?两个栈交替存储输出,不就是一个二分图嘛?那利用二分图的思想,就能轻松在n2的时间复杂度内解决这道题了。
附上代码~
关于二分图的一些技巧就在注释里面了

#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const int Inf=0x3f3f3f3f;
int n,cnt=0,not_bip=0;//cnt记录操作数,目测cnt最后会等于2*n因为每个数都要进栈和出栈 
int len1=0,len2=0;//记录两个栈的大小 
int Wait_print=1;//我们当前该输出啥数 
int dt[1010][1010];//存图 
int Sm[1010];//后缀最小值 
int sl[1010];//原数列 
int Q[1000010];//Bfs队列 
int col[1010];//每个数的颜色 
int vis[1010];//Bfs visit数组 
int stack1[1010];//1栈 
int stack2[1010];//2栈 
char Oper[2020];//操作,这个数组可以不开,大家可以在过程中直接输出,而且会更快 
void Suffix_min()//预处理后缀最小值 
{
    Sm[n]=sl[n];
    for(int i=n-1;i>=1;i--)
    {
        Sm[i]=min(Sm[i+1],sl[i]);
    }
}
void build_edge()//建边 
{ 
    memset(dt,0x3f,sizeof(dt));
    for(int i=1;i<=n;i++)
    {
        for(int j=i+1;j<=n;j++)
        {
            if(Sm[j]<sl[i]&&sl[i]<sl[j])
            {
                //满足这种条件下,单栈GG了,那么把i,j放到不同的栈里
                dt[i][j]=1;//先在ij之间添一条边 , 
                dt[j][i]=1;//等会染色就可以把他们标记上不同的颜色 
            }
        }
    }
}
void Bipartitegraph_dyeing(int poi)//这个函数名翻译成中文叫:二分图染色 
{
    //我用Bfs的方法用0/1进行染色。 
    memset(Q,0,sizeof(Q));
    int head=1,tail=0;
    Q[++tail]=poi;
    col[poi]=0;
    vis[poi]=1;
    while(head<=tail)
    {
        int st=Q[head];
        for(int i=1;i<=n;i++)
        {
            if(dt[st][i]!=Inf&&col[i]==col[st])
            {//这个条件满足,就说明该图不是二分图,
                not_bip=1;
                return ;
            }//也就是说存在了a4<a1<a2<a3这种操作 。
            if(dt[st][i]!=Inf&&col[i]==-1&&!vis[i])
            {
                col[i]=1-col[st];
                vis[i]=1;
                Q[++tail]=i;
            }
        }
        head++;
    } 
}//这样,我们就把所有数分成了col=1和col=0两类,0压1栈,1压2栈
void Simulate()//我们来模拟一下排序过程(保证字典序最小)
{
    sl[0]=Inf;//避免在len--时减到负数 
    Oper[++cnt]='a';//先把第一个数压进 1栈 
    stack1[++len1]=1;
    for(int i=2;i<=n;i++)
    {
        if(col[i]==0)
        {
            //我们要维护两个栈内的数都是单调不升的
            while(sl[i]>sl[stack1[len1]])
            {
                Oper[++cnt]='b';
                len1--;        
                Wait_print++; 
                //根据我们上面的过程,这里弹出去的一定符合顺序
                //不会把较大的数先弹出去 
            }
            Oper[++cnt]='a';
            stack1[++len1]=i;
        }
        if(col[i]==1)
        {
            //这里要注意了,因为我们要字典序最小
            //所以在往2栈填数时,先考虑一下1栈可不可以弹数 
            while(sl[stack1[len1]]==Wait_print)
            {
                Oper[++cnt]='b';
                len1--;
                Wait_print++;
            }
            while(sl[i]>sl[stack2[len2]])
            {
                Oper[++cnt]='d';
                len2--;
            }
            Oper[++cnt]='c';
            stack2[++len2]=i;
        }
    }
    while(len1>0&&len2>0)
    {
        if(sl[stack1[len1]]<=sl[stack2[len2]])
        {
            Oper[++cnt]='b';
            len1--;
        }
        else 
        {
            Oper[++cnt]='d';
            len2--;
        }
    }
    for(int i=len1;i>=1;i--)
    {
        Oper[++cnt]='b';
    }
    for(int i=len2;i>=1;i--)
    {
        Oper[++cnt]='d';
    }
}
int main()
{
    scanf("%d",&n);//读入n 
    for(int i=1;i<=n;i++)
    {
        //读入数列
        scanf("%d",&sl[i]);
    }
    Suffix_min();
    build_edge();
    memset(col,-1,sizeof(col));
    for(int i=1;i<=n;i++)
    {
        if(col[i]==-1)
        {
            Bipartitegraph_dyeing(i);
        }
    }
    if(not_bip)
    {
        printf("0\n");
        return 0;
    }
    Simulate();
    for(int i=1;i<=cnt-1;i++)
    {
        printf("%c ",Oper[i]);
    }
    printf("%c",Oper[cnt]);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值