求栈的出栈方式的个数和打印出栈顺序

无重复序列入栈的出栈方式个数

首先,出栈方式的个数为h(n)=C(2n,n)/(n+1) (n=1,2,3,…)
例如1、2、3这三个数字,入栈并出栈共有5种方式,分别为:321、312、231、213、123。
再例如,对于1、2、3、4这四个数字,4个元素的全排列共有24种,栈要求符合后进先出,按此衡量排除后即得:
1234√ 1243√ 1324√ 1342√ 1423× 1432√
2134√ 2143√ 2314√ 2341√ 2413× 2431√
3124× 3142× 3214√ 3241√ 3412× 3421√
4123× 4132× 4213× 4231× 4312× 4321√
入栈并出栈共有14种可能。
那么对于长度为n的无重复序列中所有的出栈方式有哪些呢?
1个元素进栈,有1种出栈顺序;
2个元素进栈,有2种出栈顺序;
3个元素进栈,有5种出栈顺序
我们把n个元素的出栈个数的记为f(n), 那么对于1,2,3, 我们很容易得出:
f(1) = 1 //即 1
f(2) = 2 //即 12、21
f(3) = 5 //即 123、132、213、321、231
然后我们来考虑f(4), 我们给4个元素编号为a,b,c,d,输出结果第一个出栈的位置为1号位置,第二个出栈的位置为2号位置, 那么考虑:元素a只可能出现在1号位置,2号位置,3号位置和4号位置(很容易理解,一共就4个位置,比如出栈结果为abcd,元素a就在1号位置)。
分析:
1) 如果元素a在1号位置,那么只可能a进栈之后马上出栈,此时还剩元素b、c、d等待操作,就是子问题f(3);
2) 如果元素a在2号位置,那么一定有一个元素比a先出栈,即有f(1)种可能顺序(只能是b),还剩c、d,即f(2), 根据乘法原理,无论cd什么顺序,元素a在2号位置的顺序只有f(1)种可能顺序,那么一共的顺序个数为f(1) * f(2);
3) 如果元素a在3号位置,那么一定有两个元素比1先出栈,即有f(2)种可能顺序(只能是b、c),还剩d,即f(1),根据乘法原理,一共的顺序个数为f(2) * f(1);
4) 如果元素a在4号位置,那么一定是a先进栈,最后出栈,那么元素b、c、d的出栈顺序即是此小问题的解,即 f(3);
结合所有情况,即f(4) = f(3) + f(2) * f(1) + f(1) * f(2) + f(3);
为了规整化,我们定义f(0) = 1;于是f(4)可以重新写为:
f(4) = f(0)f(3) + f(1)*f(2) + f(2) f(1) + f(3)*f(0)
然后我们推广到n,推广思路和n=4时完全一样,于是我们可以得到:
f(n) = f(0)*f(n-1) + f(1)*f(n-2) + … + f(n-1)*f(0)

f(n)=sum(0,n-1,f(i)*f(n-1-i) )
根据此递归公式,相应的动态规划代码如下(来自:http://blog.csdn.net/hacker00011000/article/details/51192390)

#include 
#include 

using namespace std;

int main()
{
    int n;
    cin >> n;
    int* arr = new int[n+1];
    memset(arr, 0, sizeof(int)*(n+1)); 
    arr[0] = 1;
    arr[1] = 1; 

    //递推关系式 
    for (int i=2; i<=n; ++i)
    {
        for (int j=0; j1-j]; 
        }
    }

    cout << arr[n] << endl;

    delete[] arr; 
    return 0;
}

由于递归公式的时间复杂度仍为O(n2),通项公式则更佳,因此,我还摘录了通项公式法的分析(来自:http://www.cnblogs.com/jiayouwyhit/p/3222973.html)
对于每一个数来说,必须进栈一次、出栈一次。我们把该数进栈设为状态‘1’,出栈设为状态‘0’。n个数的所有状态对应n个1和n个0组成的2n位二进制数。由于等待入栈的操作数按照1‥n的顺序排列、入栈的操作数b大于等于出栈的操作数a(a≤b),因此输出序列的总数目=由左而右扫描由n个1和n个0组成的2n位二进制数,1的累计数不小于0的累计数的方案种数。
(如果1的累计数小于0的累计数,则进栈元素的个数少于出栈元素,这显然不可能的。)
在2n位二进制数中填入n个1的方案数为c(2n,n),不填1的其余n位自动填0。从中减去不符合要求(由左而右扫描,0的累计数大于1的累计数)的方案数即为所求:
不符合要求的数的特征是由左而右扫描时,必然在某一奇数位2m+1位上首先出现m+1个0的累计数和m个1的累计数,此后的2(n-m)-1位上有n-m个1和n-m-1个0。如若把后面这2(n-m)-1位上的0和1互换,使之成为n-m个0和n-m-1个1,结果得1个由n+1个0和n-1个1组成的2n位数,即一个不合要求的数对应于一个由n+1个0和n-1个1组成的排列。
反过来,任何一个由n+1个0和n-1个1组成的2n位二进制数,由于0的个数多2个,2n为偶数,故必在某一个奇数位上出现0的累计数超过1的累计数。同样在后面部分0和1互换,使之成为由n个0和n个1组成的2n位数,即n+1个0和n-1个1组成的2n位数必对应一个不符合要求的数。
因而不合要求的2n位数与n+1个0,n-1个1组成的排列一一对应。
显然,不符合要求的方案数为c(2n,n+1)。
由此得出输出序列的总数目=c(2n,n)-c(2n,n+1)=c(2n,n)/(n+1)。其中,n为节点的个数
此时,通项公式得到,其成为卡塔兰数。

打印出栈顺序/打印出栈方式

为了设计打印出出栈顺序的算法,我们可以用队列(queue)来模拟输入,队列的输出则按照原先序列的顺序。使用一个栈(stack)来模拟入栈和出栈,结果保存在另外一个队列(queue)中。
现在的问题来了,怎么样可以实现所有的出栈入栈操作。
首先来看看出栈和入栈是怎么回事,对于123这个序列,1先入栈之后有两种选择,1出栈和不出栈(2入栈),而若1不出栈(2已经入栈)之后,在2出栈之前1则不能先行出栈,故对于1我们只需要考虑其在2入栈之前出栈的情况,若1在栈内时2入栈,则1与2只能看成一个整体。
这样就可以用递归的方式求解,以下为算法描述,其中input表示入栈元素的顺序,local表示模拟的栈,output表示在已经出栈的部分解。
伪代码如下:

Solution(input,local,output)
If(input.empty)
/*由于input队列已经没有内容,此时没有入栈元素的影响,local栈和output队列的内容就是最后的结果*/
    If(local.empty() )
        输出output队列的内容
    Else
        While(local栈不空)
            将local的栈顶元素出栈,放进output队列之中
        Solution(input,local,output)
    end if
Else
/*当有入栈元素时,出栈元素顺序会受到入栈元素和栈顶元素是否出栈的影响*/
    If(local.empty() ) 
        从input队列中出队列一个元素并放入local之中
        Solution(input,local,output)
    end if
Else
/*此时有两种可能,可以是local栈的栈顶元素出栈,也可以是不出栈并从input队列中取一个元素放进去,先模拟出栈,再把栈内元素放回去*/
    将local的栈顶元素出栈,暂存,并放入output队列之中
    Solution(input,local,output)
    将刚刚出栈的local的栈顶元素放回local栈之中
    将刚刚放入output队列之中的元素拿出来
    从input队列中出队列一个元素并放入local之中
    Solution(input,local,output)
/*两种方法,一种是这里的利用outputlocal进行分叉搜索,另一种搜索方法是从input中取出元素压栈(此时栈顶元素不出栈)时查找出栈顺序,然后把压入栈的元素弹出来放回input之中,然后local栈顶元素出栈,去搜索此时的出栈顺序。*/
end if
End Solution

其基本思想为对于中间栈的每一个时刻拍照,都递归其后续的所有可能,由于在递归返回的时候还需要递归前的信息,所以每次递归都是新建数据结构而保存当前时刻的状态。若输入队列已经为空,则中间栈只有一种出栈方式,中间栈也为空时递归结束。
详细代码如下:

#include 
#include  
#include 
#include 
using namespace std; 

void dfs(queue<int> input, stack<int> local, deque<int> output) {
    if (input.empty())
    {
        if (local.empty())
        {   
            cout << endl;
            int temp;
            while (!output.empty())
            {
                temp = output.back();
                output.pop_back();
                cout << temp << " ";
            } 
            cout << endl;
        }
        else
        {
            while (!local.empty())
            {
                output.push_front(local.top());
                local.pop();
            }
            dfs(input, local, output);
        }
    }
    else
    {
        if (!local.empty())
        {
            int temp = local.top();
            local.pop();
            output.push_front(temp);
            dfs(input, local, output);
            local.push(temp);
            output.pop_front();
            temp = input.front();
            input.pop();
            local.push(temp);
            dfs(input, local, output);

        }
        else
        {
            int temp = input.front();
            input.pop();
            local.push(temp);
            dfs(input, local, output);
        }
    }
}

int main() {
    queue<int> input_iterator_tag;
    for (int i = 1; i < 5; i++)
    {
        input_iterator_tag.push(i);
    } 
    dfs(input_iterator_tag, stack<int>{}, deque<int>{});
    return 0;
}

类似的问题
(1)买票找零
有2n个人排成一行进入剧场。入场费5元。其中只有n个人有一张5元钞票,另外n人只有10元钞票,剧院无其它钞票,问有多少中方法使得只要有10元的人买票,售票处就有5元的钞票找零?(将持5元者到达视作将5元入栈,持10元者到达视作使栈中某5元出栈)
(2)一个有n个1和n个-1组成的字串,且前k个数的和均不小于0,那这种字串的总数为多少?
(3)饭后,姐姐洗碗,妹妹把姐姐洗过的碗一个一个地放进碗橱摞成一摞。一共有n个不同的碗,洗前也是摞成一摞的,也许因为小妹贪玩而使碗拿进碗橱不及时,姐姐则把洗过的碗摞在旁边,问:小妹摞起的碗有多少种可能的方式?
最终结果:C(2n,n)-C(2n,n+1)
有机会再补充吧,最近刷题这么猛,还是找不到工作,人艰不拆。
参考文献:
https://zhidao.baidu.com/question/198485664882428485.html
http://www.cnblogs.com/jiayouwyhit/p/3222973.html

你可能感兴趣的:(算法题目,栈,出栈顺序,出栈方式,C++)