22. Generate Parentheses
Problem Description
You need to generate all valid combinations of parentheses given n
pairs.
A valid combination means:
- Every opening parenthesis
(
has a corresponding closing parenthesis)
- At any point while reading from left to right, the number of closing parentheses never exceeds the number of opening parentheses
For example:
- If
n = 1
, the only valid combination is"()"
- If
n = 2
, valid combinations are"(())"
and"()()"
- If
n = 3
, valid combinations include"((()))"
,"(()())"
,"(())()"
,"()(())"
, and"()()()"
The solution uses a depth-first search approach with pruning. It tracks:
l
: count of left parentheses added so farr
: count of right parentheses added so fart
: the current string being built
The key insights for pruning invalid paths:
- Never add more than
n
left or right parentheses (l > n
orr > n
) - Never have more right parentheses than left parentheses at any point (
l < r
) - When both
l
andr
equaln
, we have a valid combination
The algorithm recursively tries adding a left parenthesis dfs(l + 1, r, t + '(')
or a right parenthesis dfs(l, r + 1, t + ')')
, building all valid combinations through this exploration.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem doesn't involve nodes and edges or graph traversal. We're generating combinations of parentheses, not working with a graph structure.
Need to solve for kth smallest/largest?
- No: We're not looking for the kth smallest or largest element. We need to generate ALL valid combinations.
Involves Linked Lists?
- No: The problem doesn't involve linked list data structures or operations.
Does the problem have small constraints?
- Yes: The constraint is
n
pairs of parentheses wheren
ranges from 1 to 8. This is a small constraint that makes exhaustive search feasible. Withn = 8
, we'd have at most 16 characters (8 opening and 8 closing parentheses).
Brute force / Backtracking?
- Yes: Given the small constraints and the need to generate all valid combinations, backtracking is the appropriate approach. We systematically explore all possible ways to place parentheses while pruning invalid paths (like having more closing than opening parentheses at any point).
Conclusion: The flowchart correctly leads us to use a Backtracking approach. This aligns perfectly with the solution, which uses DFS with pruning to generate all valid parentheses combinations by:
- Recursively trying to add either
(
or)
- Backtracking when invalid conditions are met
- Collecting valid complete combinations when we've used all
n
pairs
Intuition
The key insight is recognizing that we're building strings character by character, where at each step we have a choice: add (
or add )
. This naturally forms a decision tree structure.
Think of it like this: if we need n = 2
pairs, we're placing 2 opening and 2 closing parentheses. We could try all possible arrangements like (())
, ()()
, ))((
, etc., but most would be invalid.
What makes a combination invalid? Two rules emerge:
- We can't use more than
n
of each type of parenthesis - At any point while building left-to-right, we can't have more
)
than(
(because each)
needs a matching(
before it)
This suggests a backtracking approach where we:
- Start with an empty string
- At each step, try adding
(
if we haven't used alln
opening brackets - Try adding
)
if it won't exceed the number of(
we've placed - When we've placed exactly
n
of each type, we have a valid combination
The beauty of this approach is that by checking our constraints as we build (rather than generating everything and filtering later), we avoid exploring paths that can never lead to valid solutions. For example, if we've already placed 3 closing brackets but only 2 opening brackets, we immediately know this path is invalid and backtrack.
This "build incrementally and prune early" strategy is much more efficient than generating all possible strings of length 2n
and then checking validity, especially as n
grows larger.
Learn more about Dynamic Programming and Backtracking patterns.
Solution Approach
The solution implements a depth-first search (DFS) with pruning to generate all valid parentheses combinations.
Core Algorithm Structure:
The solution uses a recursive helper function dfs(l, r, t)
where:
l
: count of left parentheses(
added so farr
: count of right parentheses)
added so fart
: the current string being built
Pruning Conditions:
The function first checks three invalid conditions to prune the search tree:
l > n
: We've used too many left parenthesesr > n
: We've used too many right parenthesesl < r
: We have more right parentheses than left (invalid at any point)
If any of these conditions are true, we immediately return without exploring further.
Base Case:
When l == n and r == n
, we've successfully placed all n
pairs of parentheses. The current string t
is a valid combination, so we add it to our answer list.
Recursive Exploration:
If we haven't reached the base case and haven't been pruned, we explore two possibilities:
- Add a left parenthesis:
dfs(l + 1, r, t + '(')
- Add a right parenthesis:
dfs(l, r + 1, t + ')')
Implementation Flow:
- Initialize an empty answer list
ans
- Start the DFS with
dfs(0, 0, '')
- no parentheses placed yet - The DFS explores all valid paths, building strings character by character
- Valid complete combinations are collected in
ans
- Return the final list of all valid combinations
The time complexity is O(4^n / √n) which represents the nth Catalan number - the actual count of valid parentheses combinations. The space complexity is O(n) for the recursion stack depth.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with n = 2
to see how it generates all valid combinations.
We start with dfs(0, 0, "")
- no parentheses placed yet.
Step 1: From dfs(0, 0, "")
- Check conditions:
l=0 ≤ 2
,r=0 ≤ 2
,l=0 ≥ r=0
✓ All valid - Not a complete solution yet (need
l=2, r=2
) - Try adding
(
: Calldfs(1, 0, "(")
- Try adding
)
: Calldfs(0, 1, ")")
Step 2: Exploring dfs(1, 0, "(")
- Conditions valid:
l=1 ≤ 2
,r=0 ≤ 2
,l=1 ≥ r=0
✓ - Try adding
(
: Calldfs(2, 0, "((")
- Try adding
)
: Calldfs(1, 1, "()")
Step 3: Exploring dfs(2, 0, "((")
- Conditions valid:
l=2 ≤ 2
,r=0 ≤ 2
,l=2 ≥ r=0
✓ - Try adding
(
: Calldfs(3, 0, "(((")
- Try adding
)
: Calldfs(2, 1, "(()")
Step 4: dfs(3, 0, "(((")
- Check:
l=3 > 2
✗ Too many left parentheses! - PRUNE - return immediately
Step 5: Exploring dfs(2, 1, "(()")
- Conditions valid:
l=2 ≤ 2
,r=1 ≤ 2
,l=2 ≥ r=1
✓ - Try adding
(
: Calldfs(3, 1, "(()(")
- Try adding
)
: Calldfs(2, 2, "(())")
Step 6: dfs(3, 1, "(()(")
- Check:
l=3 > 2
✗ Too many left parentheses! - PRUNE - return immediately
Step 7: dfs(2, 2, "(())")
- Check:
l=2 = n
andr=2 = n
✓ - Found valid combination! Add
"(())"
to answer list - Return
Step 8: Back to Step 2, now exploring dfs(1, 1, "()")
- Conditions valid:
l=1 ≤ 2
,r=1 ≤ 2
,l=1 ≥ r=1
✓ - Try adding
(
: Calldfs(2, 1, "()(")
- Try adding
)
: Calldfs(1, 2, "())")
Step 9: Exploring dfs(2, 1, "()(")
- Conditions valid:
l=2 ≤ 2
,r=1 ≤ 2
,l=2 ≥ r=1
✓ - Try adding
(
: Calldfs(3, 1, "()((")
- Try adding
)
: Calldfs(2, 2, "()()")
Step 10: dfs(3, 1, "()((")
- Check:
l=3 > 2
✗ - PRUNE - return immediately
Step 11: dfs(2, 2, "()()")
- Check:
l=2 = n
andr=2 = n
✓ - Found valid combination! Add
"()()"
to answer list - Return
Step 12: Back to Step 8, exploring dfs(1, 2, "())")
- Check:
l=1 < r=2
✗ More closing than opening! - PRUNE - return immediately
Step 13: Back to Step 1, exploring dfs(0, 1, ")")
- Check:
l=0 < r=1
✗ Starting with)
means more closing than opening! - PRUNE - return immediately
Final Result: ["(())", "()()"]
The algorithm efficiently explored only valid paths, pruning branches that would lead to invalid combinations. Out of potentially 2^4 = 16 possible arrangements of 4 parentheses, we only explored the promising paths and found the 2 valid combinations.
Solution Implementation
1class Solution:
2 def generateParenthesis(self, n: int) -> List[str]:
3 """
4 Generate all combinations of well-formed parentheses.
5
6 Args:
7 n: Number of pairs of parentheses
8
9 Returns:
10 List of all valid parentheses combinations
11 """
12 def backtrack(left_count: int, right_count: int, current_string: str) -> None:
13 """
14 Recursively build valid parentheses combinations using backtracking.
15
16 Args:
17 left_count: Number of opening parentheses used so far
18 right_count: Number of closing parentheses used so far
19 current_string: Current parentheses string being built
20 """
21 # Pruning: invalid states
22 # - Used more than n opening or closing parentheses
23 # - More closing than opening parentheses (invalid sequence)
24 if left_count > n or right_count > n or left_count < right_count:
25 return
26
27 # Base case: found a valid combination with n pairs
28 if left_count == n and right_count == n:
29 result.append(current_string)
30 return
31
32 # Try adding an opening parenthesis
33 backtrack(left_count + 1, right_count, current_string + '(')
34
35 # Try adding a closing parenthesis
36 backtrack(left_count, right_count + 1, current_string + ')')
37
38 # Initialize result list to store all valid combinations
39 result = []
40
41 # Start the backtracking process with empty string
42 backtrack(0, 0, '')
43
44 return result
45
1class Solution {
2 // List to store all valid parentheses combinations
3 private List<String> result = new ArrayList<>();
4 // Number of pairs of parentheses to generate
5 private int pairCount;
6
7 /**
8 * Generates all combinations of well-formed parentheses.
9 * @param n Number of pairs of parentheses
10 * @return List of all valid parentheses combinations
11 */
12 public List<String> generateParenthesis(int n) {
13 this.pairCount = n;
14 // Start DFS with 0 open and 0 close parentheses
15 backtrack(0, 0, "");
16 return result;
17 }
18
19 /**
20 * Recursive helper method to generate valid parentheses using backtracking.
21 * @param openCount Number of opening parentheses used so far
22 * @param closeCount Number of closing parentheses used so far
23 * @param currentString Current parentheses string being built
24 */
25 private void backtrack(int openCount, int closeCount, String currentString) {
26 // Pruning conditions:
27 // 1. Cannot use more than n opening or closing parentheses
28 // 2. Cannot have more closing than opening parentheses at any point
29 if (openCount > pairCount || closeCount > pairCount || openCount < closeCount) {
30 return;
31 }
32
33 // Base case: Found a valid combination when we've used n pairs
34 if (openCount == pairCount && closeCount == pairCount) {
35 result.add(currentString);
36 return;
37 }
38
39 // Try adding an opening parenthesis
40 backtrack(openCount + 1, closeCount, currentString + "(");
41
42 // Try adding a closing parenthesis
43 backtrack(openCount, closeCount + 1, currentString + ")");
44 }
45}
46
1class Solution {
2public:
3 vector<string> generateParenthesis(int n) {
4 vector<string> result;
5
6 // Define recursive function to generate valid parentheses combinations
7 // Parameters:
8 // - openCount: number of opening parentheses used so far
9 // - closeCount: number of closing parentheses used so far
10 // - currentString: current parentheses string being built
11 function<void(int, int, string)> backtrack = [&](int openCount, int closeCount, string currentString) {
12 // Base case: invalid conditions
13 // 1. Too many open parentheses
14 // 2. Too many close parentheses
15 // 3. More close parentheses than open (invalid sequence)
16 if (openCount > n || closeCount > n || openCount < closeCount) {
17 return;
18 }
19
20 // Valid combination found: both counts equal n
21 if (openCount == n && closeCount == n) {
22 result.push_back(currentString);
23 return;
24 }
25
26 // Try adding an opening parenthesis
27 backtrack(openCount + 1, closeCount, currentString + "(");
28
29 // Try adding a closing parenthesis
30 backtrack(openCount, closeCount + 1, currentString + ")");
31 };
32
33 // Start the recursive generation with empty string and zero counts
34 backtrack(0, 0, "");
35
36 return result;
37 }
38};
39
1/**
2 * Generates all combinations of well-formed parentheses
3 * @param n - Number of pairs of parentheses
4 * @returns Array of all valid parentheses combinations
5 */
6function generateParenthesis(n: number): string[] {
7 const result: string[] = [];
8
9 /**
10 * Depth-first search to generate valid parentheses combinations
11 * @param leftCount - Number of opening parentheses used
12 * @param rightCount - Number of closing parentheses used
13 * @param currentString - Current parentheses string being built
14 */
15 function dfs(leftCount: number, rightCount: number, currentString: string): void {
16 // Invalid state: exceeded limit or more closing than opening parentheses
17 if (leftCount > n || rightCount > n || leftCount < rightCount) {
18 return;
19 }
20
21 // Valid combination found: used all n pairs
22 if (leftCount === n && rightCount === n) {
23 result.push(currentString);
24 return;
25 }
26
27 // Try adding an opening parenthesis
28 dfs(leftCount + 1, rightCount, currentString + '(');
29
30 // Try adding a closing parenthesis
31 dfs(leftCount, rightCount + 1, currentString + ')');
32 }
33
34 // Start the recursive generation with empty string
35 dfs(0, 0, '');
36
37 return result;
38}
39
Time and Space Complexity
The time complexity is O(4^n / √n)
, which can be loosely bounded by O(4^n)
. This is because the code explores a binary tree where at each step we can either add a left parenthesis or a right parenthesis, creating a tree with maximum depth 2n
. However, due to pruning conditions (l < r
and l > n
), not all branches are explored. The actual number of valid combinations is the n
-th Catalan number, which is approximately 4^n / (n^(3/2) * √π)
. Each valid path requires O(n)
time to construct and append the string, so the total time complexity is O(4^n / √n * n)
= O(4^n * √n)
.
The reference answer O(2^(n×2) × n)
= O(2^(2n) × n)
= O(4^n × n)
is a valid upper bound since 2^(2n) = 4^n
, and multiplying by n
accounts for string operations.
The space complexity is O(n)
for the recursion call stack depth, which can go up to 2n
levels deep in the worst case, so it's O(2n)
= O(n)
. Additionally, we need to consider the space for storing the temporary string t
during recursion, which also takes O(n)
space. The output list is not counted as part of the space complexity as it's the required output. Therefore, the space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Pruning Logic Order
Pitfall: A common mistake is checking left_count < right_count
before verifying that counts are within bounds. This can lead to exploring invalid paths where right_count > n
but left_count
is even larger.
Incorrect Implementation:
def backtrack(left_count, right_count, current_string):
# Wrong: checking balance before bounds
if left_count < right_count:
return
if left_count > n or right_count > n:
return
Why it's problematic: If left_count = 4, right_count = 5, n = 3
, the first condition doesn't catch this invalid state, and we might continue processing before the second check.
Correct Implementation:
def backtrack(left_count, right_count, current_string):
# Check all invalid conditions together
if left_count > n or right_count > n or left_count < right_count:
return
2. String Concatenation Performance Issue
Pitfall: In languages where strings are immutable, repeatedly concatenating strings creates new objects each time, leading to O(n²) overhead per generated string.
Inefficient Approach:
def backtrack(left_count, right_count, current_string):
# String concatenation creates new string objects
backtrack(left_count + 1, right_count, current_string + '(')
backtrack(left_count, right_count + 1, current_string + ')')
Optimized Solution Using List:
def generateParenthesis(self, n: int) -> List[str]:
def backtrack(left_count, right_count):
if left_count > n or right_count > n or left_count < right_count:
return
if left_count == n and right_count == n:
result.append(''.join(path))
return
# Use list for O(1) append/pop operations
path.append('(')
backtrack(left_count + 1, right_count)
path.pop()
path.append(')')
backtrack(left_count, right_count + 1)
path.pop()
result = []
path = []
backtrack(0, 0)
return result
3. Missing Early Termination Optimization
Pitfall: Not implementing an optimization to stop adding closing parentheses when we've already placed all opening ones.
Suboptimal Logic:
def backtrack(left_count, right_count, current_string):
# Always tries both branches even when unnecessary
backtrack(left_count + 1, right_count, current_string + '(')
backtrack(left_count, right_count + 1, current_string + ')')
Optimized Version:
def backtrack(left_count, right_count, current_string):
if left_count == n and right_count == n:
result.append(current_string)
return
# Only add opening parenthesis if we haven't used all n
if left_count < n:
backtrack(left_count + 1, right_count, current_string + '(')
# Only add closing parenthesis if it maintains validity
if right_count < left_count:
backtrack(left_count, right_count + 1, current_string + ')')
This optimization reduces unnecessary recursive calls by checking conditions before making the recursive call rather than after.
In a binary min heap, the maximum element can be found in:
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!