22. Generate Parentheses (M)

https://leetcode.com/problems/generate-parentheses/

Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.

Example 1:

Input: n = 3
Output: ["((()))","(()())","(())()","()(())","()()()"]

Example 2:

Input: n = 1
Output: ["()"]

Constraints:

  • 1 <= n <= 8

Solution:

括号问题可以简单分成两类,一类是前文写过的 括号的合法性判断 ,一类是合法括号的生成。对于括号合法性的判断,主要是借助「栈」这种数据结构,而对于括号的生成,一般都要利用回溯递归的思想。

关于回溯算法,我们前文写过一篇 回溯算法套路框架详解 反响非常好,读本文前应该读过那篇文章,这样你就能够进一步了解回溯算法的框架使用方法了。

回到正题,括号生成算法是 LeetCode 第 22 题,要求如下:

请你写一个算法,输入是一个正整数 n,输出是 n 对儿括号的所有合法组合,函数签名如下:

比如说,输入 n=3,输出为如下 5 个字符串:

有关括号问题,你只要记住以下性质,思路就很容易想出来:

1、一个「合法」括号组合的左括号数量一定等于右括号数量,这个很好理解

2、对于一个「合法」的括号字符串组合 p,必然对于任何 0 <= i < len(p) 都有:子串 p[0..i] 中左括号的数量都大于或等于右括号的数量

如果不跟你说这个性质,可能不太容易发现,但是稍微想一下,其实很容易理解,因为从左往右算的话,肯定是左括号多嘛,到最后左右括号数量相等,说明这个括号组合是合法的。

反之,比如这个括号组合 ))((,前几个子串都是右括号多于左括号,显然不是合法的括号组合。

下面就来手把手实践一下回溯算法框架。

回溯思路

明白了合法括号的性质,如何把这道题和回溯算法扯上关系呢?

算法输入一个整数 n,让你计算 n 对儿括号能组成几种合法的括号组合,可以改写成如下问题:

现在有 2n 个位置,每个位置可以放置字符 ( 或者 ),组成的所有括号组合中,有多少个是合法的

这个命题和题目的意思完全是一样的对吧,那么我们先想想如何得到全部 2^(2n) 种组合,然后再根据我们刚才总结出的合法括号组合的性质筛选出合法的组合,不就完事儿了?

如何得到所有的组合呢?这就是标准的暴力穷举回溯框架啊,我们前文 回溯算法套路框架详解 都总结过了:

那么对于我们的需求,如何打印所有括号组合呢?套一下框架就出来了,伪码如下:

那么,现在能够打印所有括号组合了,如何从它们中筛选出合法的括号组合呢?​很简单,加几个条件进行「剪枝」就行了。

对于 2n 个位置,必然有 n 个左括号,n 个右括号,所以我们不是简单的记录穷举位置 i,而是left 记录还可以使用多少个左括号,用 right 记录还可以使用多少个右括号,这样就可以通过刚才总结的合法括号规律进行筛选了:

这样,我们的算法就完成了,算法的复杂度是多少呢?这个比较难分析,对于递归相关的算法,时间复杂度这样计算(递归次数)*(递归函数本身的时间复杂度)

backtrack 就是我们的递归函数,其中没有任何 for 循环代码,所以递归函数本身的时间复杂度是 O(1),但关键是这个函数的递归次数是多少?换句话说,给定一个 nbacktrack 函数递归被调用了多少次?

我们前面怎么分析动态规划算法的递归次数的?主要是看「状态」的个数对吧。其实回溯算法和动态规划的本质都是穷举,只不过动态规划存在「重叠子问题」可以优化,而回溯算法不存在而已。

所以说这里也可以用「状态」这个概念,对于 backtrack 函数,状态有三个,分别是 left, right, track,这三个变量的所有组合个数就是 backtrack 函数的状态个数(调用次数)。

leftright 的组合好办,他俩取值就是 0~n 嘛,组合起来也就 n^2 种而已;这个 track 的长度虽然取在 0~2n,但对于每一个长度,它还有指数级的括号组合,这个是不好算的。

说了这么多,就是想让大家知道这个算法的复杂度是指数级,而且不好算,这里就不具体展开了,是 4^n / sqrt(n),有兴趣的读者可以搜索一下「卡特兰数」相关的知识了解一下这个复杂度是怎么算的。

Last updated

Was this helpful?