打印序列的所有出栈顺序(回溯法)及复杂度分析(卡特兰数)

题目描述

对任意给定的n,输出 1,2,…,n 的所有出栈顺序。

输入

正整数 n(1≤n≤9)

输出

输出 1,2,…,n 的所有出栈顺序
示例
输入:
3
输出:
3 2 1
2 3 1
2 1 3
1 3 2
1 2 3
 

思路

这题是递归、回溯的思想,对于当前元素,只有2种操作:
(1) 进栈,处理下一个元素
(2) 栈非空,栈顶元素出栈,继续处理当前元素
注意:
(1) 47行的popVal不能是全局变量, 否则下面回溯的就不是之前的popVal了 
(2) 56,57行要回溯,这里当时没想明白为什么要回溯,要仔细思考原因,要能模拟出这个递归的过程。
 

代码实现

 1 #include <iostream>
 2 #include <vector>
 3 #include <stdio.h>
 4 #include <stack>
 5 #include <deque>
 6 
 7 using namespace std;
 8 
 9 int n;
10 
11 //因为需要遍历栈中剩余的元素,但又不能令元素出栈, 
12 //所以不能使用stack,这里把双端队列当栈使用
13 deque<int> mystack; 
14 
15 //保存当前已经出栈的元素 
16 vector<int> p;    
17 
18 void dfs(int i)
19 {
20     if(i == n+1)
21     {
22         // 打印已经出栈的元素 
23         for(int j = 0; j < p.size(); ++j)
24             printf("%d ", p[j]);
25         
26         // 打印栈中剩余的元素,因为要回溯,所以剩余的元素还不能出栈 
27         if(!mystack.empty())
28         {
29             for(int j = mystack.size()-1; j >= 0; --j)
30             {
31                 printf("%d ", mystack[j]);
32             }
33         }
34         
35         printf("
");    
36         return;
37     }
38     
39     mystack.push_back(i);    //当前元素入栈 
40     dfs(i+1);                 //处理下一个元素
41     mystack.pop_back();        //回溯
42      
43     //栈非空,栈顶元素出栈 
44     if(!mystack.empty())
45     {
46         //不能把popVal定义为全局变量,否则下面回溯的就不是之前的popVal了 
47         int popVal = mystack.back();    
48         //栈顶元素出栈 
49         mystack.pop_back();
50         //出栈的元素保存到p 
51         p.push_back(popVal);    
52         
53         dfs(i);    //继续处理当前元素
54         
55         //回溯,这里一定要回溯,不回溯会出错! 
56         p.pop_back();
57         mystack.push_back(popVal);      
58     }      
59 } 
60 
61 int main()
62 {
63     scanf("%d", &n);
64     dfs(1);
65     
66     return 0;
67 }

复杂度分析

空间复杂度:O(n)
递归的深度为n,所以空间复杂度为O(n)。
时间复杂度:
可以证明(证明过程见下文),所有的出栈顺序总数为卡特兰数,1,2,3,..,n 一共有h(n)种出栈序列,其中:
出栈次序总数为卡特兰数的证明过程:
出栈次序
一个栈(无穷大)的进栈序列为1,2,3,…,n,有多少个不同的出栈序列? 
常规分析
首先,我们设 f(n)=序列个数为n的出栈序列种数。(我们假定,最后出栈的元素为k,显然,k取不同值时的情况是相互独立的,也就是求出每种k最后出栈的情况数后可用加法原则,由于k最后出栈,因此,在k入栈之前,比k小的值均出栈,此处情况有f(k-1)种,而之后比k大的值入栈,且都在k之前出栈,因此有f(n-k)种方式,由于比k小和比k大的值入栈出栈情况是相互独立的,此处可用乘法原则,f(n-k)*f(k-1)种,求和便是Catalan递归式。)
首次出空之前第一个出栈的序数k将1~n的序列分成两个序列,其中一个是1~k-1,序列个数为k-1,另外一个是k+1~n,序列个数是n-k。
此时,我们若把k视为确定一个序数,那么根据乘法原理,f(n)的问题就等价于——序列个数为k-1的出栈序列种数乘以序列个数为n - k的出栈序列种数,即选择k这个序数的 f(n) =f(k-1) × f(n-k)。而k可以选1到n,所以再根据加法原理,将k取不同值的序列种数相加,得到的总序列种数为:f(n) = f(0)f(n-1) + f(1)f(n-2) + …… + f(n-1)f(0)。
看到此处,再看看卡特兰数的递推式,答案不言而喻,即为 f(n) = h(n) = C(2n,n) / (n+1) = C(2n,n) - C(2n,n-1)(n=0,1,2 ……)。
最后,令f(0)=1,f(1)=1。
 
非常规分析
对于每一个数来说,必须进栈一次、出栈一次。我们把进栈设为状态‘1’,出栈设为状态‘0’。n个数的所有状态对应n个1和n个0组成的2n位二进制数。由于等待入栈的操作数按照1‥n的顺序排列、入栈的操作数b大于等于出栈的操作数a(a≤b),因此输出序列的总数目=由左而右扫描由n个1和n个0组成的2n位二进制数,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)=h(n)。

问题的变种

一个有n个1和n个-1组成的字串,且前k个数的和均不小于0,那这种字串的总数为多少?
提示:将入栈操作记为1,出栈记为-1,即转化成了求所有出栈序列的问题。
原文地址:https://www.cnblogs.com/FengZeng666/p/13891463.html