questions F-L
This commit is contained in:
195
backend/data/questions/generate-parentheses.yaml
Normal file
195
backend/data/questions/generate-parentheses.yaml
Normal file
@@ -0,0 +1,195 @@
|
||||
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.
|
||||
Reference in New Issue
Block a user