Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Backtrack, building the string one character at a time while tracking how many '(' and ')' you've used. You may add '(' whenever open < n, and ')' only when close < open (so you never close a bracket you haven't opened). When the string reaches length 2n it's a complete valid combination. Pruning with those two counters means you only ever build valid strings — O(4ⁿ / √n) results, which is optimal since that's the count of answers.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you can drive a backtracking search with the right pruning conditions so only valid states are explored, and explain why those conditions guarantee validity.
Where candidates slip
Generating all 2^(2n) strings then filtering (exponentially wasteful), or getting the close condition wrong (using close < n instead of close < open), which produces invalid strings like '))((' .
How to stand out
State the two invariants up front, explain that close < open is what enforces balance, and note the output size is the n-th Catalan number so the algorithm is output-optimal.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewstate: cur, open, closelen(cur) == 2n?YesNoExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Brute Force — Generate All, Then Filter (Baseline)
1Hint 1Explore hintHide hint
Ignoring validity, how many strings of length 2n can you form from '(' and ')'?
2Hint 2Explore hintHide hint
Generate all 2^(2n) such strings, then keep only the balanced ones using a counter check.
3Hint 3Explore hintHide hint
Correct but wasteful: most of those strings are invalid, and you still pay to build and validate each.
A · Core idea
Enumerate every length-2n string over {'(', ')'} and keep those that are balanced (running count never negative, ends at zero).
Why mention it. It cleanly separates 'generate' from 'validate' and confirms what a valid string is. Why we reject it. It builds 2^(2n) candidates but only Catalan(n) are valid — for n = 8 that's 65536 candidates for 1430 answers, and the ratio worsens as n grows. Backtracking folds the validity check into generation so invalid prefixes are never extended.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
n = 1"(("
ends with count 2 ≠ 0 → invalid
discard["()"]C · Code with teaching comments
Reference implementation previewpublic List<String> generateParenthesis(int n) { List<String> result = new ArrayList<>(); int len = 2 * n; for (int mask = 0; mask < (1 << len); mask++) { StringBuilder sb = new StringBuilder();Explore the full codeHide the full code
D · Why it works and complexityO(2^(2n) · n) timeO(2^(2n) · n) spaceOpen the invariant and proofExplore the proofHide the proof
Whenever brute force is 'generate everything then filter', ask whether the filter can be applied incrementally during construction. If a partial candidate can be judged invalid early, backtracking prunes whole subtrees of wasted work.
ComplexityO(2^(2n) · n) time, O(2^(2n) · n) space — There are 2^(2n) binary strings of length 2n; validating each costs O(n). The vast majority are discarded.
E · Edge cases and pitfalls2O(2^(2n)) work to produce only Catalan(n) results.Explore edge casesHide edge cases
Pitfalls
- O(2^(2n)) work to produce only Catalan(n) results.
- Building full strings before any validity check, wasting time and memory.
Approach 2 of 2 · Optimal
Backtracking with Count Pruning (Optimal)
1Hint 1Explore hintHide hint
Build the string character by character. What's the rule that keeps a partial string a valid PREFIX of some balanced string?
2Hint 2Explore hintHide hint
Track open and close counts. You can add '(' while open < n; you can add ')' only while close < open.
3Hint 3Explore hintHide hint
When the length hits 2n the string is complete and necessarily valid — record it. The two count rules mean you never build an invalid prefix.
A · Core idea
Grow a candidate string left to right, recursing on the two possible next characters while enforcing constraints that keep every partial string a valid prefix.
State. Carry the current string cur and two counters: open (number of '(' placed) and close (number of ')' placed).
The two pruning invariants. (1) Add '(' only when open < n — we can never use more than n opening brackets. (2) Add ')' only when close < open — closing is allowed only if there is an unmatched opener to close. This second condition is the heart of the algorithm: it guarantees the running balance open − close is never negative, which is exactly the property that makes a prefix extendable to a balanced string. Strings like )( or ()) are never even started.
Base case. When len(cur) == 2n, we've placed n of each bracket in a valid order, so cur is a complete well-formed combination — append it to the results and return. No separate validity check is needed because the invariants guaranteed it throughout.
Why no explicit 'undo'. In the string-passing form, each recursive call receives a fresh cur + '(' or cur + ')', so there's nothing to revert — the immutability does the backtracking for us. (With a shared StringBuilder/list you'd append before recursing and delete the last character after, the classic make-move / undo-move pattern.)
Why it's optimal. You cannot do asymptotically better than producing the output, and the number of valid strings is the n-th Catalan number C(n) = (2n)! / ((n+1)! n!) ≈ 4ⁿ/(n√(πn)). Since the algorithm visits only valid prefixes (each internal node leads to at least one leaf), its running time is proportional to the output size times O(n) per string — output-optimal.
Relation to stacks. Validity here is the same invariant a stack checks in Valid Parentheses: balance never negative, zero at the end. Backtracking is effectively exploring all strings that a stack-validator would accept, generated directly rather than tested after the fact.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
n = 2Array state
(0,0)
add '(' (open<2)
["(())", "()()"]C · Code with teaching comments
D · Why it works and complexityO(4ⁿ / √n) timeO(n) recursion depth (plus output) spaceOpen the invariant and proofExplore the proofHide the proof
Backtracking = DFS over partial solutions with early pruning. The reusable move is to find invariants that a partial candidate must satisfy and check them before recursing, so entire invalid subtrees are skipped. Here those invariants (open ≤ n, close ≤ open) turn an exponential filter into an output-optimal generator — the same discipline solves Subsets, Permutations, Combination Sum, and N-Queens.
ComplexityO(4ⁿ / √n) time, O(n) recursion depth (plus output) space — The recursion's leaves are exactly the Catalan(n) valid strings; each takes O(n) to assemble. Catalan(n) ≈ 4ⁿ/(n√(πn)), so total work is O(4ⁿ/√n). Recursion depth is 2n → O(n) stack space beyond the output.
E · Edge cases and pitfalls8n = 1 — Only path: '(' then ')' → ["()"]. close<open blocks a leading ')'.Explore edge casesHide edge cases
Edge cases handled
n = 1Only path: '(' then ')' → ["()"]. close<open blocks a leading ')'.n = 8Produces Catalan(8) = 1430 strings; recursion depth 16, still trivial.trying ')' firstImpossible at the root since close(0) < open(0) is false → no invalid prefix ever begins.open == close mid-buildclose < open is false, so ')' is blocked — only '(' can extend, preventing premature closing.
Pitfalls
- Using close < n instead of close < open — generates unbalanced strings like '))(('.
- Forgetting to undo the StringBuilder edit in the shared-buffer version — corrupts sibling branches.
- Adding a post-hoc validity filter — unnecessary and signals you didn't trust the pruning.
- Recomputing counts from the string each call instead of passing them — needless O(n) work per node.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic List<String> generateParenthesis(int n) { List<String> result = new ArrayList<>(); backtrack(result, new StringBuilder(), 0, 0, n); return result; }Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Wrong close condition
Allowing ')' whenever close < n (instead of close < open) lets you close brackets you never opened, producing strings like ')(' or '())(' that aren't well-formed.
Explore the code comparisonHide the code comparison
Wrong close condition
Allowing ')' whenever close < n (instead of close < open) lets you close brackets you never opened, producing strings like ')(' or '())(' that aren't well-formed.
Don't write this
if close_count < n: # WRONG
backtrack(cur + ')', open_count, close_count + 1)Write this instead
if close_count < open_count: # only close an opened bracket
backtrack(cur + ')', open_count, close_count + 1)2Missing undo with a shared buffer
When mutating one StringBuilder across recursive calls, you must remove the character you appended after returning, or the next branch inherits stale characters.
Explore the code comparisonHide the code comparison
Missing undo with a shared buffer
When mutating one StringBuilder across recursive calls, you must remove the character you appended after returning, or the next branch inherits stale characters.
Don't write this
if (open < n) {
cur.append('(');
backtrack(result, cur, open + 1, close, n);
// forgot to delete the appended '('
}Write this instead
if (open < n) {
cur.append('(');
backtrack(result, cur, open + 1, close, n);
cur.deleteCharAt(cur.length() - 1);
}Revise
Carry the pattern into the interview
Pattern to remember
Backtracking with open/close count constraints
Code pattern to memorize
def backtrack(cur, open, close):
if len(cur) == 2*n: res.append(cur); return
if open < n: backtrack(cur+'(', open+1, close)
if close < open: backtrack(cur+')', open, close+1)Rules to remember
- 1Add
'('only whileopen < n— you can't open more than n pairs. - 2Add
')'only whileclose < open— never close more than you've opened (the validity invariant). - 3Base case: string length
== 2nmeans a complete, guaranteed-valid combination → record it. - 4These two count checks prune invalid branches before they're built, so no post-hoc validity check is needed.
- 5The recursion tree's leaves are exactly the Catalan(n) valid strings — that's why the output count is C(n) = O(4ⁿ / (n√n)).
Use this pattern when
- Enumerate **all** valid combinations/configurations, not just count or check one
- Choices are constrained and you can prune invalid partial states early
- Balanced-structure generation: brackets, valid IPs, well-formed sequences
Choose another pattern when
- You only need the *count* of valid strings → that's the Catalan number, a closed form, no enumeration
- You only need to *validate* one string → that's Valid Parentheses with a stack/counter
- n is large and you can't materialize all results → enumeration is exponential by nature
Interview takeaway
The key is the two counter invariants that keep every partial string a valid prefix: open ≤ n and close ≤ open. Pruning at construction time (not validating afterward) is the essence of backtracking and reappears in subsets, permutations, and combination-sum.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternBacktracking with constraint pruning. Recognise it when you must enumerate all valid configurations and can reject invalid partial states early. The open ≤ n / close ≤ open invariants are this problem's pruning rules; the skeleton is shared with Subsets, Permutations, and Combination Sum.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Valid ParenthesesValidate one string with a stack — the invariant this generator enforces.
- SubsetsComing soonSame backtracking skeleton — include/exclude each element.
- Combination SumComing soonBacktracking with a running sum and a pruning bound.
- PermutationsComing soonBacktracking with a used-set; make-move / undo-move pattern.