questions F-L

This commit is contained in:
2025-05-25 11:47:04 +01:00
parent ecf95bd23d
commit 917c371529
54 changed files with 11235 additions and 0 deletions

View 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
&nbsp;
**Step 2: Identify base case**
- When `len(current) == 2 * n`, we've placed all parentheses
- Add the completed string to our results list
&nbsp;
**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)
&nbsp;
**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
&nbsp;
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.