[LeetCode] 22. Generate Parentheses
2 min read
🧠 Remember: “Never close more than you open.”
The problem — 20 seconds
n = 3 → ["((()))","(()())","(())()","()(())","()()()"] (any order accepted).
Well-formed means every ) matches an earlier unmatched (.
"((()))" ✓ · "(()))(" ✗ — the 4th char closes what was never opened
The fix isn’t filtering bad strings out — it’s never growing them. That pruning is the algorithm. Everything below just names why.
Your first instinct
Generate all 2^(2n) bracket strings, keep the balanced ones. For n = 3 that’s 64 candidates for 5 answers; for n = 10 it’s a million candidates for 16,796 answers. The old version of this post had the right recursion but muddled the trace (it walked an n = 2 tree while claiming n = 3 results, and its left > right guard hid which rule fires when).
The turning point
Two rules decide everything, and both are checks on counts — not on the string:
- add
(only whileopen < n— brackets left to spend; - add
)only whileclose < open— never close more than you opened.
The second rule prunes invalid branches the moment they appear: a ) with close == open can never recover, so that whole subtree never exists.
Watch it work
Press Play. Green leaves are the 5 valid strings; red ✗ stubs are branches rejected on the spot — ")" at the root, "())", "(()))", "()())". Watch the open/close counters at every step.
Interactive visual · Pruned decision tree
Open freely, close carefully — invalid branches die early
Each step adds ( or ), but two rules prune the tree: open needs open < n, close needs close < open. Red ✗ branches are rejected the moment close > open — five valid leaves survive for n = 3.
collected valid strings
if open < n: dfs(open+1, close, path + "(")if close < open: dfs(open, close+1, path + ")")if open == close == n: ans.append(path) # validQuick check — make it stick
When may you add a closing bracket?
🧠 Memory hook: “Never close more than you open.” That one check prunes every invalid branch the moment it appears.
What the code is really saying
class Solution:
def generateParenthesis(self, n: int) -> list[str]:
ans: list[str] = []
def dfs(open: int, close: int, path: str) -> None:
if open == close == n:
ans.append(path) # valid leaf
return
if open < n:
dfs(open + 1, close, path + "(")
if close < open: # the pruning rule — invalid branches die here
dfs(open, close + 1, path + ")")
dfs(0, 0, "")
return ans
Read it as the visual: leaf check, open-branch, guarded close-branch — the same order you just watched. Note the guard order matters: close < open (not close < n) is what keeps every prefix valid.
Why it works
Invariant: close <= open <= n holds at every call, so every prefix is completable to something valid. A leaf (open == close == n) is therefore valid by construction, every valid string is reachable by following its own characters, and nothing invalid is ever built — pruning replaces filtering.
n = 3 → 5 valid (Catalan C₃) · 4 branches pruned on sight
Time O(4ⁿ/√n) — proportional to valid strings, not all 2²ⁿ candidates.
🧠 Remember
Parentheses → “Never close more than you open.”
open < nspends,close < openguards — the guard is the prune.
🔁 Try this: what changes for multiple bracket types (()[]{})? (Hint: the counter becomes a stack — validity needs which is open, not just how many.)
Related: Permutations · Subsets