Problem Overview
LeetCode 22 — Generate Parentheses — gives you an integer n and asks you to generate all combinations of well-formed (valid) parentheses using exactly n pairs. A string of parentheses is well-formed if every open parenthesis has a matching close parenthesis and the brackets are properly nested. For n=1 there is one result: "()", for n=2 there are two: "(())" and "()()", and for n=3 there are five.
The problem is rated Medium on LeetCode and appears in the NeetCode 150 stack and backtracking sections. The naive approach of generating all 2^(2n) binary strings and filtering for validity is too slow. The key insight is to build valid strings character by character during backtracking — only extend when the extension keeps the string valid. This prunes the search space dramatically.
Understanding this problem gives you the backtracking template for constraint-guided generation. The same open/close counting pattern reappears in Valid Parentheses, Remove Invalid Parentheses, and Score of Parentheses. The result count — the nth Catalan number — connects this problem to a surprising family of combinatorial structures.
- Input: integer n (1 <= n <= 8)
- Output: list of all valid combinations of n pairs of parentheses
- n=1: ["()"]
- n=2: ["(())", "()()"]
- n=3: ["((()))", "(()())", "(())()", "()(())", "()()()"]
- No duplicates; return in any order
Backtracking Framework
The backtracking approach builds the result string one character at a time. It tracks two counters: open (how many "(" have been added so far) and close (how many ")" have been added so far). At each recursive call, you choose to add either "(" or ")" based on two simple rules: add "(" if open < n, add ")" if close < open. When the string reaches length 2n, it is a complete valid combination and is added to the results.
The base case is len(current) == 2*n, at which point the current string is guaranteed to be valid — the two constraints ensure no invalid string can ever be completed. The recursion tree has at most 2^(2n) leaves before pruning, but the constraints cut it down to only the Catalan number of valid leaves. Each valid leaf is reached exactly once, and the path to it is the unique valid string.
This is fundamentally different from a "generate then filter" approach. You never build an invalid string — the constraints prevent it at every step. Every leaf in the pruned recursion tree is a valid answer, and every valid answer corresponds to exactly one leaf. The time complexity is O(4^n / sqrt(n)) — the sum of lengths of all results, which matches the nth Catalan number times 2n.
- 1Initialize result list, call backtrack(current="", open=0, close=0)
- 2Base case: if len(current) == 2*n, append current to results and return
- 3If open < n: recurse with current + "(", open+1, close
- 4If close < open: recurse with current + ")", open, close+1
Two simple rules govern validity: add "(" if open < n, add ")" if close < open — these two conditions generate only valid strings
You never need to validate the completed string because the constraints prevent invalid states from ever forming. If open < n you can safely add an open paren — you still have room for more. If close < open you can safely add a close paren — there is an unmatched open to close. These two guards together mean: (1) you never use more than n open parens, and (2) you never close something that was not opened. Any string built under these rules is guaranteed to be valid when it reaches length 2n. This is the essence of constraint-guided backtracking — prune invalid paths early rather than filter results at the end.
Decision Tree
Visualize the backtracking as a binary decision tree. Each node holds the current string and the (open, close) counts. From any node, you branch left by adding "(" (if open < n) and branch right by adding ")" (if close < open). Nodes where neither branch is available and the string is incomplete are dead ends — they are pruned and never reached under the constraint rules.
For n=2, the tree starts at ("", 0, 0). From there: add "(" → ("(", 1, 0). From that: add "(" → ("((", 2, 0), or add ")" → ("()", 1, 1). From ("((", 2, 0): can only add ")" → ("(()", 2, 1) → "(())" (complete). From ("()", 1, 1): add "(" → ("()(", 2, 1) → "()()" (complete). The tree naturally produces exactly 2 leaves for n=2, both valid.
Each leaf in the tree sits at depth 2n and represents one valid combination. Internal nodes at depth less than 2n always have at least one valid child — the constraints guarantee the tree never gets stuck at a non-leaf before reaching depth 2n. The number of leaves equals the nth Catalan number: 1, 2, 5, 14, 42 for n = 1, 2, 3, 4, 5.
- 1Root: (current="", open=0, close=0)
- 2At each node: branch left with "(" if open < n, branch right with ")" if close < open
- 3Leaf condition: len(current) == 2n — add to results
- 4Dead-end condition: open == n and close == open at depth < 2n — impossible (means string is already complete)
- 5Tree has exactly Catalan(n) leaves, each a distinct valid combination
Catalan Number Connection
The number of valid combinations of n pairs of parentheses is the nth Catalan number: C(n) = (2n)! / ((n+1)! * n!). For small values: C(1) = 1, C(2) = 2, C(3) = 5, C(4) = 14, C(5) = 42, C(6) = 132. These are the exact counts you see when you run Generate Parentheses for each n. The growth rate is approximately 4^n / (n^1.5 * sqrt(pi)), meaning the output size grows exponentially — you cannot do better than O(Catalan(n)) time since you must output every result.
The Catalan numbers arise whenever you count the number of ways to correctly match n pairs of something. For parentheses, each valid string corresponds to a unique "ballot sequence" — a sequence where at every prefix, the number of "(" is >= the number of ")". This is the same as counting paths from (0,0) to (n,n) on a grid that never cross the diagonal, which equals C(n).
The closed-form formula C(n) = (2n)! / ((n+1)! * n!) can also be written as C(n) = (1/(n+1)) * binomial(2n, n). For n=3: (1/4) * (6! / (3! * 3!)) = (1/4) * 20 = 5. For n=4: (1/5) * (8! / (4! * 4!)) = (1/5) * 70 = 14. These numbers grow roughly as 4^n / n^1.5, so for n=8 (the LeetCode constraint maximum) there are C(8) = 1430 valid combinations.
The Catalan number appears in many combinatorial problems: valid parentheses, binary trees, polygon triangulation, and Dyck paths are all counted by it
The nth Catalan number C(n) counts: the number of valid parenthesizations of n+1 factors (associativity choices), the number of full binary trees with n+1 leaves, the number of triangulations of a convex polygon with n+2 vertices, and the number of monotonic lattice paths from (0,0) to (n,n) that do not pass above the diagonal (Dyck paths). All of these problems share the same recursive structure: a "split" or "matching" that divides the problem into two independent sub-problems. The parentheses recurrence C(n) = sum(C(i)*C(n-1-i) for i in 0..n-1) is the Catalan recurrence, and it is also the recurrence that appears in the DP solution to Generate Parentheses.
Iterative DP Alternative
An iterative dynamic programming approach builds valid strings of length 2n bottom-up from valid strings of smaller sizes. Define dp[k] as the list of all valid parenthesizations for k pairs. The base case is dp[0] = [""]. For each k from 1 to n, dp[k] is constructed by iterating over all splits i from 0 to k-1: for each i, every string in dp[i] can be wrapped inside outer parens and concatenated with every string in dp[k-1-i]. The formula is dp[k] += "(" + p + ")" + q for all p in dp[i] and q in dp[k-1-i].
This recurrence reflects the Catalan structure: every valid string of n pairs begins with "(" and the matching ")" appears somewhere in the middle, splitting the string into an "inner" part (i pairs) and a "tail" part (n-1-i pairs). By iterating over all possible positions of the first matching close paren, you generate every valid string exactly once.
The DP approach produces the same result as backtracking — the same Catalan number of strings — but avoids recursion entirely. It is less intuitive for most interview settings because the recurrence requires understanding the Catalan decomposition, but it can be useful when the call stack depth is a concern or when building on previous results is advantageous. Both approaches have the same asymptotic time complexity of O(4^n / sqrt(n)).
- 1dp[0] = [""]
- 2For k from 1 to n:
- 3 dp[k] = []
- 4 For i from 0 to k-1:
- 5 For p in dp[i], q in dp[k-1-i]: dp[k].append("(" + p + ")" + q)
- 6Return dp[n]
Code Walkthrough Python and Java
Python solution using backtracking: def generateParenthesis(n): res = []; def bt(s, open, close): if len(s) == 2*n: res.append(s); return; if open < n: bt(s+"(", open+1, close); if close < open: bt(s+")", open, close+1); bt("", 0, 0); return res. The function builds the string with concatenation s+"(" and s+")" rather than a mutable list with append/pop — this is cleaner because strings are immutable and each recursive call receives its own copy of s without needing explicit backtracking (no pop needed). Time O(4^n / sqrt(n)), space O(n) for the recursion stack depth (the deepest path is 2n frames).
Java solution: public List<String> generateParenthesis(int n) { List<String> res = new ArrayList<>(); bt(res, "", 0, 0, n); return res; } private void bt(List<String> res, String s, int open, int close, int n) { if (s.length() == 2*n) { res.add(s); return; } if (open < n) bt(res, s+"(", open+1, close, n); if (close < open) bt(res, s+")", open, close+1, n); }. Java String concatenation creates a new String object at each call, which is acceptable here since strings are short (length at most 2*8 = 16 for n <= 8) and the total work is bounded by the Catalan number.
Both solutions have identical logic: the two if-guards replace explicit validity checking, string concatenation replaces StringBuilder with explicit undo, and the base case appends the completed string. The Python solution is about 8 lines, the Java solution about 10. Both pass all LeetCode 22 test cases with excellent performance — the pruned backtracking tree is much smaller than the full 2^(2n) binary tree. For n=8, only 1430 strings are generated out of 2^16 = 65,536 possible binary strings of length 16.
Use string concatenation or StringBuilder, not a list of chars with append/pop; string concat is cleaner since strings are immutable and small
A common backtracking pattern uses a mutable list and explicitly appends a character then pops it after the recursive call: path.append("("); bt(...); path.pop(). This works but requires careful undo logic. For Generate Parentheses, since strings are short (max 2*8 = 16 chars) and immutable, it is simpler to pass s+"(" and s+")" directly — each recursive call gets its own string and no explicit undo is needed. This trades a tiny amount of memory (new string objects per call) for much cleaner code. For longer strings where copying is expensive, the append/pop pattern with a list or StringBuilder is preferable, but for n <= 8 the string concat approach is idiomatic and fast.