这道题也算是很经典的了,属于一个基本原理题,深刻理解了这种题,也就理解了一大堆相似的问题。
分析:n对括号组成的合法字符串,那第一个字符肯定是“(”,然后和它配对的“)”可能出现在第2,4,6……2n个字符的位置。所以,当n是3时,合法字符串共有6个,可以认为是以下字符串的集合:
()()(),()(()) 和第一个“(”配对的“)”在第2个位置
(())() 和第一个“(”配对的“)”在第4个位置
(()()),((())) 和第一个“(”配对的“)”在第6个位置
所以,假设h(n)为所求函数,那么
h(n)= h(0)*h(n-1) + h(1)*h(n-2) + + h(n-1)h(0) (其中n>=1),而且h(0)=1
这个数就是卡特兰(Catalan)数,它是组合数学中一个常出现在各种计数问题中出现的数列。由以比利时的数学家欧仁•查理•卡塔兰 (1814–1894)命名。
下面推导怎么计算卡特兰数:
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+1个1,n-1个-1
我们再观察这个翻转后的序列,对于有n+1个1,n-1个-1的排列,共有C(n+1,2n)种。而对于这种非法的排列:
总是存在一个最小的k,我们只需要从第1个到第k个元素翻转回去,就能变成对于有n个1,n个-1的情况下的非法排列。同样,每一个n个1,n个-1的情况下的非法排列也会对应一个n+1个1,n-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,完全按推导来:
vectorgenerate(int n){
vector result;
if(n==0){
result.push_back("");
return result;
}
if(n==1){
result.push_back("()");
return result;
}
for(int i=1;i<=n;i++){
vectorleft=generate(i-1);
vectorright=generate(n-i);
for(int j=0;j
我们可以看到这个方法重复算了很多generate(i),因此时间复杂度偏高。而且要是采用动态规划类似的方式存储中间结果的话,空间复杂度又会爆棚。所以,完全按公式来不是一个最好的方法。
方法二:
主要是位置0必须放“(”,后面的位置可以尝试着放“(”或“)”,然后就进入递归。
基本准则:在合法括号序列构建的每一步,都需要“(”的个数大于等于“)”的个数。
这个算法不是很好想,算是一种改良的DFS吧。
[cpp] view plaincopyprint?
1. vector
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 8. f0(s+")",l,r-1); 9. } 10. if(l>0){ 11. f0(s+"(",l-1,r); 12. } 13. } 14. vector 15. string s=""; 16. f0(s,n,n); 17. return result; 18. }
注意:
1. f0的输入参数s不是引用,因为s需要时一个栈上的变量,每次调用都是一次copy
2. 不要写成:
[cpp] view plaincopyprint?
1. if(l 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的值,以防影响到下次的调用。