196 lines
9.2 KiB
YAML
196 lines
9.2 KiB
YAML
title: Generate Parentheses
|
|
slug: generate-parentheses
|
|
difficulty: medium
|
|
leetcode_id: 22
|
|
leetcode_url: https://leetcode.com/problems/generate-parentheses/
|
|
categories:
|
|
- strings
|
|
- recursion
|
|
patterns:
|
|
- backtracking
|
|
|
|
description: |
|
|
Given `n` pairs of parentheses, write a function to *generate all combinations of well-formed parentheses*.
|
|
|
|
A well-formed parentheses string has equal numbers of opening and closing parentheses, with every closing parenthesis matching a preceding opening one.
|
|
|
|
constraints: |
|
|
- `1 <= n <= 8`
|
|
|
|
examples:
|
|
- input: "n = 3"
|
|
output: '["((()))","(()())","(())()","()(())","()()()"]'
|
|
explanation: "All 5 valid combinations of 3 pairs of parentheses."
|
|
- input: "n = 1"
|
|
output: '["()"]'
|
|
explanation: "With just one pair, there's only one valid combination."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine you're building a string character by character, and at each step you can choose to add either an opening `(` or a closing `)` parenthesis.
|
|
|
|
The key insight is that not every choice is valid. A string of parentheses is **well-formed** if at any point while reading left-to-right, the number of closing parentheses never exceeds the number of opening ones. Think of it like a balance: each `(` adds +1 to the balance, and each `)` subtracts 1. The balance must never go negative.
|
|
|
|
This naturally leads to a **decision tree** approach. At each position, we branch based on what characters we *can* legally add:
|
|
- We can add `(` if we haven't used all `n` opening parentheses yet
|
|
- We can add `)` if we have more opening parentheses than closing ones (i.e., there's an unmatched `(` to close)
|
|
|
|
By exploring all valid paths through this decision tree, we generate exactly the set of well-formed parentheses strings — no more, no less.
|
|
|
|
approach: |
|
|
We solve this using **Backtracking** — systematically building candidates and abandoning paths that can't lead to valid solutions.
|
|
|
|
**Step 1: Define the recursive state**
|
|
|
|
- `current`: The string we're building
|
|
- `open_count`: Number of `(` parentheses used so far
|
|
- `close_count`: Number of `)` parentheses used so far
|
|
|
|
|
|
|
|
**Step 2: Identify base case**
|
|
|
|
- When `len(current) == 2 * n`, we've placed all parentheses
|
|
- Add the completed string to our results list
|
|
|
|
|
|
|
|
**Step 3: Define recursive choices**
|
|
|
|
- **Add `(`**: Only if `open_count < n` (we haven't used all opening parentheses)
|
|
- **Add `)`**: Only if `close_count < open_count` (there's an unmatched `(` to close)
|
|
|
|
|
|
|
|
**Step 4: Backtrack after each choice**
|
|
|
|
- After exploring a path, the recursion naturally "unwinds"
|
|
- Since we pass strings (immutable in Python), backtracking is implicit
|
|
- With mutable structures, you'd explicitly remove the last character
|
|
|
|
|
|
|
|
The constraints on when we can add each character ensure we only generate valid combinations, making this more efficient than generating all permutations and filtering.
|
|
|
|
common_pitfalls:
|
|
- title: Generating All Permutations Then Filtering
|
|
description: |
|
|
A naive approach might generate all possible strings of `(` and `)` characters, then filter for valid ones.
|
|
|
|
With `n = 8`, that's `2^16 = 65,536` strings to generate and validate, but only `1,430` are valid (the 8th Catalan number). This wastes significant computation on invalid strings.
|
|
|
|
The backtracking approach only explores valid paths, never generating invalid strings in the first place.
|
|
wrong_approach: "Generate all 2^(2n) strings, filter valid ones"
|
|
correct_approach: "Use constraints during generation to only build valid strings"
|
|
|
|
- title: Forgetting the Close Constraint
|
|
description: |
|
|
It's tempting to think you can always add a `)` as long as you haven't used all `n` of them. But consider building with `n = 2`:
|
|
|
|
Starting with `()`, if you add `)` next you get `())` — this is invalid because the third character closes a parenthesis that was never opened.
|
|
|
|
The rule is: you can only add `)` when `close_count < open_count`, not just when `close_count < n`.
|
|
wrong_approach: "Add ) whenever close_count < n"
|
|
correct_approach: "Add ) only when close_count < open_count"
|
|
|
|
- title: Modifying Strings In-Place Incorrectly
|
|
description: |
|
|
In languages with mutable strings or when using a list to build the string, forgetting to backtrack (remove the last character after recursion) leads to corrupted results.
|
|
|
|
In Python, passing `current + '('` creates a new string, so backtracking is automatic. But if using a list like `current.append('(')`, you must call `current.pop()` after the recursive call returns.
|
|
|
|
key_takeaways:
|
|
- "**Backtracking pattern**: Build solutions incrementally, using constraints to prune invalid paths early"
|
|
- "**Decision tree thinking**: Visualise recursive problems as trees where each node is a choice point"
|
|
- "**Catalan numbers**: The count of valid parentheses combinations follows the Catalan sequence — this appears in many combinatorial problems"
|
|
- "**Constraint propagation**: Encoding validity rules into the recursion conditions is more efficient than post-hoc filtering"
|
|
|
|
time_complexity: "O(4^n / √n). This is the n<sup>th</sup> Catalan number, representing the count of valid combinations. Each valid string takes O(n) to construct."
|
|
space_complexity: "O(n). The recursion depth is at most `2n` (the length of each string), and we store the current string being built."
|
|
|
|
solutions:
|
|
- approach_name: Backtracking
|
|
is_optimal: true
|
|
code: |
|
|
def generate_parenthesis(n: int) -> list[str]:
|
|
result = []
|
|
|
|
def backtrack(current: str, open_count: int, close_count: int):
|
|
# Base case: we've placed all 2n parentheses
|
|
if len(current) == 2 * n:
|
|
result.append(current)
|
|
return
|
|
|
|
# Choice 1: Add opening parenthesis if we haven't used all n
|
|
if open_count < n:
|
|
backtrack(current + '(', open_count + 1, close_count)
|
|
|
|
# Choice 2: Add closing parenthesis if it won't make string invalid
|
|
if close_count < open_count:
|
|
backtrack(current + ')', open_count, close_count + 1)
|
|
|
|
backtrack('', 0, 0)
|
|
return result
|
|
explanation: |
|
|
**Time Complexity:** O(4^n / √n) — The number of valid sequences is the n<sup>th</sup> Catalan number.
|
|
|
|
**Space Complexity:** O(n) — Recursion stack depth plus the current string being built.
|
|
|
|
We recursively build strings by making valid choices at each step. The constraints (`open_count < n` and `close_count < open_count`) ensure we never explore invalid paths, making this efficient despite the exponential output size.
|
|
|
|
- approach_name: Iterative with Stack
|
|
is_optimal: false
|
|
code: |
|
|
def generate_parenthesis(n: int) -> list[str]:
|
|
result = []
|
|
# Stack holds tuples of (current_string, open_count, close_count)
|
|
stack = [('', 0, 0)]
|
|
|
|
while stack:
|
|
current, open_count, close_count = stack.pop()
|
|
|
|
# Base case: complete string
|
|
if len(current) == 2 * n:
|
|
result.append(current)
|
|
continue
|
|
|
|
# Add closing parenthesis option first (will be processed second due to LIFO)
|
|
if close_count < open_count:
|
|
stack.append((current + ')', open_count, close_count + 1))
|
|
|
|
# Add opening parenthesis option
|
|
if open_count < n:
|
|
stack.append((current + '(', open_count + 1, close_count))
|
|
|
|
return result
|
|
explanation: |
|
|
**Time Complexity:** O(4^n / √n) — Same as recursive, we explore all valid paths.
|
|
|
|
**Space Complexity:** O(4^n / √n) — The stack can hold many partial solutions simultaneously.
|
|
|
|
This converts the recursion to an explicit stack, which can be useful in languages with limited recursion depth. The logic is identical — we just manage the call stack manually. Note that space complexity is worse because we store all pending states on the heap rather than using the call stack.
|
|
|
|
- approach_name: Dynamic Programming
|
|
is_optimal: false
|
|
code: |
|
|
def generate_parenthesis(n: int) -> list[str]:
|
|
# dp[i] contains all valid strings with i pairs of parentheses
|
|
dp = [[] for _ in range(n + 1)]
|
|
dp[0] = [''] # Base case: empty string for 0 pairs
|
|
|
|
for i in range(1, n + 1):
|
|
# Build strings for i pairs using smaller subproblems
|
|
# Pattern: "(" + dp[j] + ")" + dp[i-1-j] for all valid j
|
|
for j in range(i):
|
|
for left in dp[j]:
|
|
for right in dp[i - 1 - j]:
|
|
dp[i].append('(' + left + ')' + right)
|
|
|
|
return dp[n]
|
|
explanation: |
|
|
**Time Complexity:** O(4^n / √n) — We generate all Catalan(n) strings.
|
|
|
|
**Space Complexity:** O(4^n / √n) — We store all valid strings for all values up to n.
|
|
|
|
This builds solutions bottom-up. For `i` pairs, we consider all ways to split: `j` pairs inside the first `()` and `i-1-j` pairs after it. While correct, this uses more memory than backtracking since it stores all intermediate results.
|