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

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

题目描述

对任意给定的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

2 #include

3 #include

4 #include

5 #include

6

7 using namespace std;

8

9 int n;

10

11 //因为需要遍历栈中剩余的元素,但又不能令元素出栈,

12 //所以不能使用stack,这里把双端队列当栈使用

13 deque mystack;

14

15 //保存当前已经出栈的元素

16 vector 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)种出栈序列,其中:

出栈次序总数为卡特兰数的证明过程:

来源:卡特兰数 - 百度百科

出栈次序

一个栈(无穷大)的进栈序列为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,即转化成了求所有出栈序列的问题。

相关文章