TL;DR
Backtracking with open/close counters, emitting only valid strings β O(4^n / βn) time (proportional to the Catalan-sized output), O(n) extra space beyond the output.
Approach 1 β Brute force
Generate all 2^(2n) strings of ( and ) and keep the valid ones, checking validity with a running counter (a degenerate stack: the stack would only ever hold (, so its height is enough).
from itertools import product
class Solution:
def generateParenthesis(self, n: int) -> list[str]:
def valid(cand: tuple[str, ...]) -> bool:
height = 0
for ch in cand:
height += 1 if ch == "(" else -1
if height < 0:
return False
return height == 0
out: list[str] = []
for cand in product("()", repeat=2 * n):
if valid(cand):
out.append("".join(cand))
return out
Complexity: O(2^(2n) Β· n) time, O(n) working space.
Why the constraints kill it: at n = 8 thatβs 65,536 candidates of length 16 β survivable, but only ~2% of them are valid; the wasted 98% grows without bound and the approach dies immediately past the constraint. Generating only valid strings is the point of the exercise.
Approach 2 β Backtracking on the validity invariant
The insight: a partial string can be extended to a valid one iff opens <= n and closes <= opens. So instead of validating after the fact, enforce the invariant while building: at each step you may add ( if opens < n, and ) if closes < opens. Every path in this pruned tree ends in a valid string β zero waste. This is backtracking: depth-first construction of partial solutions with undo, abandoning branches the moment they canβt lead to an answer (here, invalid branches are never even entered).
class Solution:
def generateParenthesis(self, n: int) -> list[str]:
res: list[str] = []
path: list[str] = []
def backtrack(opens: int, closes: int) -> None:
if len(path) == 2 * n:
res.append("".join(path))
return
if opens < n:
path.append("(")
backtrack(opens + 1, closes)
path.pop() # undo before trying ')'
if closes < opens:
path.append(")")
backtrack(opens, closes + 1)
path.pop()
backtrack(0, 0)
return res
Walkthrough for n = 2: the root must open β path ( (opens 1). From there both moves are legal: opening again gives ((, which can only close twice β (()); closing instead gives (), which must open (closes are not < opens), giving ()( and then ()(). The recursion tree has exactly 2 leaves, ["(())", "()()"] β it never touched the other 14 strings of length 4.
Complexity: the number of valid strings is the nth Catalan number C(2n, n)/(n+1) β the count of balanced bracketings β which grows as Ξ(4^n / n^1.5); total work is O(4^n / βn) counting the O(n) copy per leaf. Extra space O(n) for the path and recursion depth (which acts as the stack in this βstack patternβ problem).
Approach 3 β Dynamic programming on the first closure
The insight: every valid string decomposes uniquely as "(" + A + ")" + B, where A is the (valid) content inside the first (βs matching ), and B is the (valid) remainder. If A uses i pairs, B uses n β 1 β i. Build answers bottom-up from smaller n β classic dynamic programming: solve each subproblem once and combine stored results.
class Solution:
def generateParenthesis(self, n: int) -> list[str]:
dp: list[list[str]] = [[""]] # dp[0]: one empty string
for k in range(1, n + 1):
cur: list[str] = []
for i in range(k): # pairs inside the first group
for inner in dp[i]:
for rest in dp[k - 1 - i]:
cur.append("(" + inner + ")" + rest)
dp.append(cur)
return dp[n]
Walkthrough for n = 2: dp[1] is built from i = 0: "(" + "" + ")" + "" β ["()"]. dp[2] takes i = 0: "()" ++ rest β dp[1] β "()()", and i = 1: "(" + "()" + ")" β "(())". Result ["()()", "(())"] β the same 2 strings.
Complexity: output-linear like backtracking, O(4^n / βn) time; O(4^n / βn) space because all smaller levels are retained (a real cost backtracking doesnβt pay).
Common pitfalls
- Checking
closes < n instead of closes < opens β that generates invalid strings like ")("; the closing move is legal only against an unmatched (.
- Forgetting to undo (
path.pop()) when using a shared mutable buffer β siblings inherit a corrupted prefix. Passing immutable strings down avoids this at some copying cost.
- Deduplicating with a set β unnecessary; both the backtracking tree and the first-closure decomposition produce each string exactly once.
- Calling the complexity O(2^n) or O(n!) β the tight answer is the Catalan asymptotic Ξ(4^n / n^1.5); interviewers often probe this.
Pattern takeaway
When asked to enumerate all structures satisfying a prefix-checkable invariant, push the validity check into the generator: recurse only on extensions that keep the invariant, and every leaf is an answer. The open/close counter is a stack reduced to its height β the same balance invariant behind Valid Parentheses β and the backtracking recursion is itself the stack pattern in disguise.