【卡特兰数】有N对括号,输出所有合法的组合

这道题也算是很经典的了,属于一个基本原理题,深刻理解了这种题,也就理解了一大堆相似的问题。
分析:n对括号组成的合法字符串,那第一个字符肯定是“(”,然后和它配对的“)”可能出现在第2,4,6……2n个字符的位置。所以,当n3时,合法字符串共有6个,可以认为是以下字符串的集合:

()()(),()(())                   和第一个“(”配对的“)”在第2个位置

()()                                                和第一个“(”配对的“)”在第4个位置

()()(())                   和第一个“(”配对的“)”在第6个位置

所以,假设hn)为所求函数,那么

h(n)= h(0)*h(n-1) + h(1)*h(n-2) +   + h(n-1)h(0) (其中n>=1),而且h0=1
这个数就是卡特兰(Catalan)数,它是组合数学中一个常出现在各种计数问题中出现的数列。由以比利时的数学家欧仁•查理•卡塔兰 (18141894)命名。


下面推导怎么计算卡特兰数:

1.    考虑n对括号,共有n(n个“)”。
对于其全排列,可以看做是2n个空,将n ( 放入其中任意n个空中,剩余的位置由 ) 填充,显然其全排列的个数为 C(n,2n)

2.    在全排列中,包含一部分非法的排列,我们从中减去非法的排列个数,即可得到合法的排列数目。问题规模被缩小。考虑所有排列中,非法排列的个数。

3.    先来观察非法排列的特性,我们假设(1)-1,对于任意一个非法排列a1,a2 ... an ,比存在一个k,使得
          a1+a2+a3..ak<0

因为如果这个和小于0,说明到k位置-1出现的次数比1多,即右括号出现的次数比左括号多,即该组合是非法

4.    对于一个非法排列,必存在一个k,使得a1+a2+a3..ak<0,给出一个n=3时具体的排列:
           1, -1,1, -1
-1, 1
   
k=5时,出现了非法情况。
   
我们将1~5元素翻转,即1-1置换,那么该序列就变成了
           -1, 1,-1, 1, 1, 1

   这个翻转的序列中,有n+11n-1-1
   
我们再观察这个翻转后的序列,对于有n+11n-1-1的排列,共有C(n+1,2n)种。而对于这种非法的排列:
 
总是存在一个最小的k,我们只需要从第1个到第k个元素翻转回去,就能变成对于有n1n-1的情况下的非法排列。同样,每一个n1n-1的情况下的非法排列也会对应一个n+11n-1-1的排列。
   
例如:
        1, 1, 1, 1, -1, -1--->
k=1翻转 -1,1,1,1,-1,-1 
        -1, 1, 1, 1, 1, -1--->
k=2翻转 1,-1,-1,1,1,-1
(这里不是很容易理解,需要自己画图分析)

5.    所以可以推得,非法排列的个数为C(n+1,2n),最终可得结论:

对于n对括号,合法的排列共有C(n,2n) - C(n+1,2n).
也就是(2n)!/(n+1)!(n)!, 也就是C(n,2n)/(n+1)

那么,怎么打印出所有的括号组合呢?

方法一:DFS,完全按推导来:

vector<string>generate(int n){
    vector<string> result;
    if(n==0){
        result.push_back("");
        return result;
    }
    if(n==1){
        result.push_back("()");
        return result;
    }
    for(int i=1;i<=n;i++){
        vector<string>left=generate(i-1);
        vector<string>right=generate(n-i);
        for(int j=0;j<left.size();j++){
            for(int k=0;k<right.size();k++){
               result.push_back("("+left[j]+")"+right[k]);
            }
        }
    }
    return result;
}


我们可以看到这个方法重复算了很多generatei),因此时间复杂度偏高。而且要是采用动态规划类似的方式存储中间结果的话,空间复杂度又会爆棚。所以,完全按公式来不是一个最好的方法。

方法二:
主要是位置0必须放“(”,后面的位置可以尝试着放“(”或“)”,然后就进入递归。
基本准则:在合法括号序列构建的每一步,都需要“(”的个数大于等于“)”的个数。

这个算法不是很好想,算是一种改良的DFS吧。

 [cpp] view plaincopyprint?

1.   vector<string> result;  

2.   void f0(string s,int l,int r){  

3.       if(l==0 && r==0){  

4.           result.push_back(s);  

5.           return;  

6.       }  

7.       if(l<r){  

8.           f0(s+")",l,r-1);  

9.       }  

10.      if(l>0){  

11.          f0(s+"(",l-1,r);  

12.      }  

13.  }  

14.  vector<string> f(int n){  

15.      string s="";  

16.      f0(s,n,n);  

17.      return result;  

18.  }  

注意:

1. f0的输入参数s不是引用,因为s需要时一个栈上的变量,每次调用都是一次copy

2. 不要写成:

[cpp] view plaincopyprint?

1.   if(l<r){  

2.       s=s+")";  

3.       f0(s,l,r-1);  

4.   }  

5.   if(l>0){  

6.      s=s+"(";  

7.       f0(s,l-1,r);  

8.   }  


因为插入左括号和右括号是并列的关系,是两种平行的情况,所以不能改变s的值,以防影响到下次的调用。




  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值