Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses. This classic problem—LeetCode #22: Generate Parentheses—is a foundational milestone for software engineers mastering recursive depth-first search (DFS) and state-space backtracking. Beyond interview algorithms, understanding well-formed bracket generation is essential for building compiler parsers, abstract syntax tree (AST) tokenizers, and validating mathematical expression grammars.
Problem Statement & Examples
A parenthesis string is well-formed if and only if every opening bracket ( has a corresponding closing bracket ), and at no point reading from left to right does the count of closing brackets exceed opening brackets.
Example 1:
Input: n = 3 Output: ["((()))","(()())","(())()","()(())","()()()"]
Example 2:
Input: n = 1 Output: ["()"]
Mathematical Combinatorics: Catalan Numbers & Dyck Paths
How many valid combinations exist for a given n? The total count of well-formed parentheses strings of length 2n is given by the n-th Catalan Number ($C_n$):
In discrete mathematics, this problem is equivalent to counting Dyck Paths: monotonic lattice paths on a 2D grid from $(0, 0)$ to $(2n, 0)$ consisting of steps $(1, 1)$ (representing an open parenthesis) and $(1, -1)$ (representing a close parenthesis) that never dip below the horizontal x-axis ($y = 0$). Any path that drops below the axis corresponds to an illegal prefix where closing parentheses outnumber opening parentheses.
For small values of n, the sequence grows as follows:
n = 1: $C_1 = 1$ ("()")n = 2: $C_2 = 2$ ("(())","()()")n = 3: $C_3 = 5$ (5 combinations)n = 4: $C_4 = 14$n = 8: $C_8 = 1,430$
Asymptotically, $C_n \sim \frac{4^n}{n^{3/2} \sqrt{\pi}}$. This mathematical bound confirms that any algorithm generating all combinations must take at least $O\left(\frac{4^n}{\sqrt{n}}\right)$ time.
State Machine & Backtracking Tree
To generate only valid combinations and avoid exploring dead-end permutations, we enforce two invariant rules at every recursive step:
- Rule 1 (Add Open): We can append
(whenever the count of open brackets placed so far is strictly less thann(open < n). - Rule 2 (Add Close): We can append
)if and only if the count of close brackets is strictly less than open brackets (close < open).
Iterative Dynamic Programming Decomposition
While backtracking DFS is the most intuitive and memory-efficient approach, understanding the Dynamic Programming formulation deepens algebraic mastery of the problem. Every non-empty well-formed parenthesis string $S$ can be factored uniquely as:
where $A$ is a well-formed parenthesis string of $k$ pairs ($0 le k le n - 1$), and $B$ is a well-formed parenthesis string of $(n - 1 - k)$ pairs. This allows bottom-up table building:
- For $n = 0$: $DP[0] = [, "" ,]$
- For $n = 1$: $k = 0 implies "(" + DP[0] + ")" + DP[0] = [, "()" ,]$
- For $n = 2$: $k = 0 implies "(" + DP[0] + ")" + DP[1] = [, "()()" ,]$; $k = 1 implies "(" + DP[1] + ")" + DP[0] = [, "(())" ,]$
While DP requires additional memory to store intermediate subproblem lists, it eliminates recursion stack overhead and showcases the direct isomorphism between grammar production rules and dynamic programming.
Modern C++20 Implementation (Zero-Allocation Buffer Reuse)
In early implementations, recursive algorithms frequently pass string copies by value or utilize std::function closures, incurring unnecessary heap allocations. The optimal production implementation uses a mutable reference buffer with push_back and pop_back, avoiding string copying until a valid leaf is reached:
#include <iostream>
#include <vector>
#include <string>
class Solution {
public:
std::vector<std::string> generateParenthesis(int n) {
std::vector<std::string> result;
std::string current;
current.reserve(2 * n); // Pre-allocate buffer capacity
backtrack(n, 0, 0, current, result);
return result;
}
private:
void backtrack(int n, int openCount, int closeCount,
std::string& current, std::vector<std::string>& result) {
// Base case: string has reached maximum length (2 * n)
if (static_cast<int>(current.length()) == 2 * n) {
result.push_back(current);
return;
}
// Branch 1: Add opening parenthesis if quota remains
if (openCount < n) {
current.push_back('(');
backtrack(n, openCount + 1, closeCount, current, result);
current.pop_back(); // Backtrack state
}
// Branch 2: Add closing parenthesis if valid
if (closeCount < openCount) {
current.push_back(')');
backtrack(n, openCount, closeCount + 1, current, result);
current.pop_back(); // Backtrack state
}
}
};
int main() {
Solution solver;
int n = 3;
std::vector<std::string> solutions = solver.generateParenthesis(n);
std::cout << "Total solutions for n = " << n << ": " << solutions.size() << "\n";
for (const auto& s : solutions) {
std::cout << " " << s << "\n";
}
return 0;
}
Production Engineering: Compiler Parsers & AST Tokenizers
The logic underpinning parenthesis generation is not merely an interview parlor trick; it is the fundamental core of syntax analyzers in compilers and build tools:
- HTML / JSX Tag Balancing: When compilers like Babel or SWC parse JSX elements, they track opening and closing tags in a LIFO structure directly analogous to parentheses validation. An unclosed
<div>or mismatched closing tag triggers a syntax error through identical invariant checks. - Mathematical Expression Evaluators: In Dijkstra's Two-Stack Shunting-Yard algorithm for converting infix expressions to Reverse Polish Notation (RPN), matching parentheses dictate operator precedence and subexpression tree evaluation.
Complexity Analysis
| Metric | Complexity | Mathematical Justification |
|---|---|---|
| Time Complexity | O(4^n / sqrt(n)) |
Bounded by the n-th Catalan number $C_n = \frac{1}{n+1}\binom{2n}{n}$. Each valid string of length $2n$ takes $O(n)$ to copy to the output. |
| Auxiliary Space | O(n) |
The recursion depth is exactly $2n$. Using a single mutable string buffer requires only $O(n)$ stack and heap space (excluding output array). |
