打印序列的所有出栈顺序(回溯法)及复杂度分析(卡特兰数)
题目描述
对任意给定的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("\n"); 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)种出栈序列,其中:
出栈次序总数为卡特兰数的证明过程:
来源:卡特兰数 - 百度百科
出栈次序
常规分析
首先,我们设 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,即转化成了求所有出栈序列的问题。