假设有一个入栈序列 a 1 , a 2 , ⋯   , a n a_1,a_2,\cdots,a_n a1,a2,,an,现在求它有多少种出栈序列。

例如,对于入栈序列 1 , 2 , 3 1,2,3 1,2,3,我们有 5 5 5种出栈序列:
① 3 , 2 , 1 ①3,2,1 3,2,1push 1,push 2,push 3,pop 3,pop 2,pop 1
② 2 , 3 , 1 ②2,3,1 2,3,1push 1,push 2,pop 2,push 3,pop 3,pop 1
③ 1 , 3 , 2 ③1,3,2 1,3,2push 1,pop 1,push 2,push 3,pop 3,pop 2
④ 1 , 2 , 3 ④1,2,3 1,2,3push 1,pop 1,push 2,pop 2,push 3,pop 3
⑤ 2 , 1 , 3 ⑤2,1,3 2,1,3push 1,push 2,pop 2,pop 1,push 3,pop 3

现在我们考虑对任意 n n n的情况。令 f ( i ) f(i) f(i)代表长度为 i i i的序列的出栈序列个数。则我们要求的就是 f ( n ) f(n) f(n)

显然, f ( 0 ) = f ( 1 ) = 1 f(0)=f(1)=1 f(0)=f(1)=1,因为没有元素和只有一个元素的时候都只有一种顺序。有 n n n个元素时,问题太大,我们采用分而治之的思想,递归求解。考虑入栈的第一个元素 a 1 a_1 a1在出栈序列中的位置。设 a 1 a_1 a1是第 j j j个出栈的,则 a 2 ∼ a j a_2\sim a_j a2aj必须在 a 1 a_1 a1之前出栈。这个性质是十分关键的。要理解这个性质,必须运用栈的LIFO (Last In First Out)(后进先出)特征。因为 a 2 ∼ a j a_2\sim a_j a2aj a 1 a_1 a1后入栈,那就比 a 1 a_1 a1先出栈。换句话说, a 2 ∼ a j a_2\sim a_j a2aj一定是压在 a 1 a_1 a1头顶的,如果它们不出去, a 1 a_1 a1就没法出去。当 a 1 a_1 a1出去时,栈一定是空的。那么下面就简单了。现在 a 1 a_1 a1把出栈序列分割为两部分 a 2 ∼ a j a_2\sim a_j a2aj a j + 1 ∼ a n a_{j+1}\sim a_n aj+1an。我们看到,前一部分和后一部分是相互独立的。而前一部分的出栈顺序个数为 f ( j − 1 ) f(j-1) f(j1),后面一部分的出栈顺序个数为 f ( n − j ) f(n-j) f(nj)。因为二者互不影响,所以贡献的总出栈顺序个数为 f ( j − 1 ) × f ( n − j ) f(j-1)\times f(n-j) f(j1)×f(nj)如果你不理解,可以先考虑前一部分有一个固定的顺序,那么后一部分有 f ( n − j ) f(n-j) f(nj)种顺序;再令前一部分的顺序变化,共有 f ( j − 1 ) f(j-1) f(j1)种变化,所以结果就是 f ( j − 1 ) f(j-1) f(j1) f ( n − j ) f(n-j) f(nj),即 f ( j − 1 ) × f ( n − j ) f(j-1)\times f(n-j) f(j1)×f(nj)好了,这就是对于一个固定的 j j j的顺序个数。再令 j j j取到 1 ∼ n 1\sim n 1n,用加法原理把所有可能性加起来,就是 f ( n ) = f ( 0 ) f ( n − 1 ) + f ( 1 ) f ( n − 2 ) + ⋯ + f ( n − 1 ) f ( 0 ) = ∑ j = 1 n f ( j − 1 ) f ( n − j ) \begin{aligned}f(n)&=f(0)f(n-1)+f(1)f(n-2)+\cdots+f(n-1)f(0)\\&=\sum\limits_{j=1}^nf(j-1)f(n-j)\end{aligned} f(n)=f(0)f(n1)+f(1)f(n2)++f(n1)f(0)=j=1nf(j1)f(nj)这里要求 n ≥ 2 n\ge2 n2。这个递推公式解起来挺麻烦的,我们直接给出结论。事实上, f ( n ) f(n) f(n)就是卡特兰数(Catalan Numbers),记作 C n C_n Cn(不要与组合数混淆QwQ),它的的公式是 C n = ( 2 n ) ! ( n + 1 ) ! n ! = 1 n + 1 C 2 n n C_n=\frac{(2n)!}{(n+1)!n!}=\frac{1}{n+1}C_{2n}^{n} Cn=(n+1)!n!(2n)!=n+11C2nn可以验证它满足我们推出的递推公式。当 n = 3 n=3 n=3时, C 3 = 6 ! 4 ! 3 ! = 720 24 × 6 = 120 24 = 5 C_3=\frac{6!}{4!3!}=\frac{720}{24\times6}=\frac{120}{24}=5 C3=4!3!6!=24×6720=24120=5,与我们在开头提到的 1 , 2 , 3 1,2,3 1,2,3 5 5 5种出栈序列相符。


出栈序列的个数问题与很多问题等价。例如,求 n n n个左括号和 n n n个右括号能组成多少合法的括号序列。这里的合法指的是每个括号都有配对。 n = 3 n=3 n=3时有 5 5 5种: ( ( ( ) ) ) ,   ( ( ) ( ) ) ,   ( ( ) ) ( ) ,   ( ) ( ) ( ) ,   ( ) ( ( ) ) ((())),\ (()()),\ (())(),\ ()()(),\ ()(()) ((())), (()()), (())(), ()()(), ()(()),恰与 3 3 3个元素的出栈序列个数相同。这种括号序列问题的答案也是卡特兰数。


最后附上求任一入栈序列的所有出栈序列的代码:

#include <iostream>
#include <string>
#include <vector>

using namespace std;

int n, ans;
string s, a;
vector<char> stk;

void dfs(int i)
{
    // --- pop
    if(!stk.empty())
    {
        a.push_back(stk.back());
        if(a.size() == n)
            cout << a << '\n', ++ans;
        stk.pop_back();
        dfs(i);
        stk.push_back(a.back());
        a.pop_back();
    }
    // --- push
    if(i < n)
    {
        stk.push_back(s[i]);
        dfs(i + 1);
        stk.pop_back();
    }
}

int main()
{
    cout << "Please enter the in-stack "
        "sequence:\n>>> ";
    // e.g. ABCED
    cin >> s;
    n = s.size();
    dfs(0);
    cout << "Totally " << ans << " sequences.\n";
    return 0;
}

例如

Please enter the in-stack sequence:
>>> ABCD
ABCD
ABDC
ACBD
ACDB
ADCB
BACD
BADC
BCAD
BCDA
BDCA
CBAD
CBDA
CDBA
DCBA
Totally 14 sequences.
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐