Problem
Implement an algorithm to print all valid (i.e., properly opened and closed) combinations of n-pairs of parentheses.
OR
Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.
Follow up
Make sure the returned list of strings are sorted.
Examples
Example 1:
|
|
Example 2:
|
|
Solution
This is a classic combinatorial problem, appearing in various forms. These strings, known as Dyck strings (see more here), consist of ( n ) X’s and ( n ) Y’s, ensuring no initial segment has more Y’s than X’s. Examples include:
|
|
Similar problems include:
- Generating all balanced parenthetical expressions for ( N ) pairs (i.e., this problem)
- Creating all possible ways to apply a binary operator to ( N + 1 ) factors
- Forming all full binary trees with ( N + 1 ) leaves
Solution
Method 1 - Backtracking
We have to keep track of open and close parentheses. Main idea is that we should add ')' only after valid '('. So, we can take 2 integer variables - open and close, to keep track of open ( and close ) parentheeis. Then our recursion follows the rule:
- if
openis less thann, we can add(to the current string - if
closeis less thanopen, we can add)to the current string
Here is how the recursion tree will look:
graph TD
A[" "]
A --> |"("| O["("]
O --> |"("| O2["(("]
O --> |")"| OC["()"]
O2 --> |"("| O3["((("]
O3 --> |")"| O3C["((()"]
O3C --> |")"| O3C2["((())"]
O3C2 --> |")"| O3C3["((()))"]:::answer
OC --> |"("| OCO["()("]
OCO --> |"("| OCO2["()(("]
OCO2 --> |")"| OCO2C["()(()"]
OCO2C --> |")"| OCO2C2["()(())"]:::answer
O2 --> |")"| O2C["(()"]
O2C --> |"("| O2CO["(()("]
O2CO --> |")"| O2COC["(()()"]
O2COC --> |")"| O2COC2["(()())"]:::answer
OCO --> |")"| OCOC["()()"]
OCOC --> |"("| OCOCO["()()("]
OCOCO --> |"("| OCOCOC["()()()"]:::answer
O2C --> |")"| O2C2["(())"]
O2C2 --> |"("| O2C2O["(())("]
O2C2O --> |"("| O2C2O2["(())()"]:::answer
classDef answer fill:green,stroke:#333,stroke-width:2px;
Code
|
|
|
|
|
|
Complexity
- ⏰ Time complexity:
O(2^n)because at each step, we either open or close a parenthesis. That is the upper bound. But actually, if we do the counting, when we count number of valid parentheses, it is equal to catalan number, i.e.C(n) = 2n! / (n + 1)!(n!). More on it: Catalan Numbers. - 🧺 Space complexity:
O(n)assuming recursion stack
Method 2 - Top Down DP
Here’s how you can implement this approach:
-
Recursive Function with Memoization:
- Use a dictionary (or a map) to store previously computed results for specific numbers of pairs.
- Recursively generate combinations by placing pairs in every valid position.
- Memoize results to avoid recomputation.
-
Base Case:
- When (n = 0), the only result is an empty string.
Try to visualize the below code with memo map:
|
|
Code
|
|
|
|
Method 3 - Bottom up DP
We use a dynamic programming table where each entry dp[i] represents all possible combinations of i pairs of well-formed parentheses. Let dp[i] represents all well-formed combinations of i pairs of parentheses.
Recurrence relation
To get dp[i], you can think of it as placing a pair of parentheses () in every possible position around combinations of smaller pairs:
$$
dp[i] = \sum_{k=0}^{i-1} \text{( “(” + dp[k] + “)” + dp[i-1-k] )}
$$
This means placing dp[k] inside (), and then concatenating it with all valid combinations of dp[i-1-k].
Code
|
|
|
|
Method 4 - Level Order Traversal
Let’s start with the base cases:
- n = 0: The result set is empty (no solution).
- n = 1: The result set is {()}.
For n = 2, we can build the result set by inserting a new pair “()” into all gaps in each result from n = 1. For example, the only solution for n = 1 is () with 3 gaps, as shown:
|
|
To get the result for n = 2, we append “()” in each gap of the solutions from n = 1, leading to:
|
|
The unique result set for n = 2 is {()(), (())}. Generally, there are m+1 gaps in a string of length m. We can construct a string with n parentheses by inserting “()” into each of the m+1 gaps in the solutions of length m for n-1.
To implement this, formulate the problem as a tree traversal problem. Starting from n = 1 as the root “()”, we traverse m+1 = 3 children corresponding to the string constructed by inserting “()” into the m+1 gaps. The nodes at level n of this tree will contain all possible strings with n pairs of parentheses. For instance:
|
|
We can perform a level order traversal using a queue and stop at level = n, at which point the queue will contain the result set for n. For more details on the level order traversal algorithm using a queue, refer to my previous note on finding the rightmost cousin of a binary tree. The following is a simple implementation of this idea.
Code
|
|