LeetCode 1087 - Brace Expansion
In this problem, we are given a string that represents multiple possible words. Each position in the word may contain either a single fixed character or a set of alternative characters enclosed in curly braces.
Difficulty: 🟡 Medium
Topics: String, Backtracking, Stack, Breadth-First Search, Sorting
Solution
Problem Understanding
In this problem, we are given a string that represents multiple possible words. Each position in the word may contain either a single fixed character or a set of alternative characters enclosed in curly braces.
For example:
a{b,c}d
means:
- The first character is always
'a' - The second character can be either
'b'or'c' - The third character is always
'd'
So the valid generated words are:
["abd", "acd"]
The task is to generate every possible word that can be formed by choosing one option from each position, then return the results sorted in lexicographical order.
The input format has several guarantees that simplify the problem:
- There are no nested braces.
- Every brace group contains distinct lowercase letters.
- The string is always valid.
- The maximum length is only 50 characters.
These constraints tell us that parsing the string is relatively straightforward. Since there are no nested braces, we never need recursive parsing logic or stack-based expression evaluation. We only need to identify whether the current position is:
- a single character, or
- a comma-separated option group.
The major challenge is generating all combinations correctly and efficiently.
An important observation is that the total number of generated strings can grow exponentially. For example:
{a,b,c}{d,e,f}{g,h,i}
already produces:
3 × 3 × 3 = 27
results.
This means any correct solution must ultimately spend time proportional to the number of generated words.
Some important edge cases include:
- Strings with no braces at all, such as
"abcd" - A brace group appearing at the beginning or end
- Multiple consecutive brace groups
- Single-option groups like
"{a}", even though uncommon - Lexicographical ordering requirements
The problem guarantees valid syntax, so we do not need to handle malformed brace expressions.
Approaches
Brute Force Approach
The brute force idea is to recursively try every possible interpretation directly from the raw string without preprocessing.
At each character:
- If the character is a normal letter, append it to the current string.
- If the character is
'{', scan forward until'}', extract all options, and recursively branch for each one.
This works because every valid word corresponds to exactly one sequence of choices from each position.
However, this approach repeatedly scans portions of the string during recursion. Every recursive branch may need to search for closing braces and repeatedly parse comma-separated options. That introduces unnecessary repeated work.
Although the constraints are small enough that this would still pass, it is not the cleanest or most efficient design.
Optimal Approach
The key insight is that the problem naturally separates into two phases:
- Parse the input into groups of character choices
- Generate all combinations using backtracking
For example:
"{a,b}c{d,e}"
can first be transformed into:
[
['a', 'b'],
['c'],
['d', 'e']
]
Once we have this structure, the problem becomes a classic Cartesian product generation problem.
Backtracking is ideal here because:
- Each position contributes one character
- We build strings incrementally
- Every recursive level corresponds to one character position
- We explore every valid combination exactly once
Sorting becomes easy if we sort each option group before backtracking. Since recursion explores choices in order, the generated results automatically appear lexicographically sorted.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(N × K) | O(K) | Repeatedly reparses sections of the string during recursion |
| Optimal | O(N + K) | O(K) | Parses once, then generates combinations efficiently |
Where:
N= length of input stringK= total size of all generated output strings
Since output generation itself is unavoidable, K dominates the complexity.
Algorithm Walkthrough
Step 1: Parse the String into Option Groups
We iterate through the input string from left to right.
If the current character is a lowercase letter:
- Create a single-option group containing that character.
If the current character is '{':
- Continue scanning until
'}' - Split the contents by commas
- Sort the extracted options
- Store them as one group
For example:
"{a,b}c{d,e}"
becomes:
[
['a', 'b'],
['c'],
['d', 'e']
]
This preprocessing step makes the generation phase much simpler.
Step 2: Use Backtracking to Build Words
We recursively process one group at a time.
At recursion level i:
- Choose one character from
groups[i] - Append it to the current partial word
- Recurse to the next group
- Remove the character afterward to backtrack
This explores every possible combination.
Step 3: Store Complete Words
When the recursion depth equals the number of groups:
- We have selected one character from every position
- Join the accumulated characters into a complete word
- Add it to the result list
Step 4: Return the Result
Because groups and options were processed in sorted order, the generated results are already lexicographically sorted.
No additional sorting step is required.
Why it works
The algorithm works because every generated word corresponds to choosing exactly one option from each parsed group.
The recursion systematically explores all such choices without duplication:
- Each recursive level represents one position in the final word.
- Every valid option for that position is explored.
- Backtracking restores state correctly before trying the next option.
Since all combinations are explored exactly once, and choices are processed in sorted order, the algorithm generates every valid word in lexicographical order.
Python Solution
from typing import List
class Solution:
def expand(self, s: str) -> List[str]:
groups = []
i = 0
# Parse the input into groups
while i < len(s):
if s[i] == '{':
j = i
while s[j] != '}':
j += 1
options = s[i + 1:j].split(',')
options.sort()
groups.append(options)
i = j + 1
else:
groups.append([s[i]])
i += 1
result = []
path = []
# Backtracking
def backtrack(index: int) -> None:
if index == len(groups):
result.append(''.join(path))
return
for char in groups[index]:
path.append(char)
backtrack(index + 1)
path.pop()
backtrack(0)
return result
The implementation begins by parsing the string into a list of groups. Each group contains all valid characters for one position in the final word.
For brace expressions, we scan until the closing brace, split by commas, sort the options, and store them together. For normal letters, we simply create a one-element group.
After parsing, we use a recursive backtracking function.
The path list stores the current partially constructed word. At each recursion level, we iterate through all possible characters for the current position.
When we reach the end of all groups, we join the characters in path into a string and append it to the result.
The append and pop operations implement classic backtracking state management. Each recursive call temporarily adds a character, explores deeper combinations, then restores the previous state.
Because all options are sorted before recursion starts, the generated results naturally appear in lexicographical order.
Go Solution
package main
import (
"sort"
"strings"
)
func expand(s string) []string {
var groups [][]string
// Parse the input
for i := 0; i < len(s); {
if s[i] == '{' {
j := i
for s[j] != '}' {
j++
}
options := strings.Split(s[i+1:j], ",")
sort.Strings(options)
groups = append(groups, options)
i = j + 1
} else {
groups = append(groups, []string{string(s[i])})
i++
}
}
var result []string
var path []string
var backtrack func(int)
backtrack = func(index int) {
if index == len(groups) {
result = append(result, strings.Join(path, ""))
return
}
for _, ch := range groups[index] {
path = append(path, ch)
backtrack(index + 1)
path = path[:len(path)-1]
}
}
backtrack(0)
return result
}
The Go implementation follows the same overall algorithm as the Python version.
One notable difference is that Go strings are immutable, so the current word is stored as a slice of strings called path. When a full word is constructed, strings.Join combines the characters.
Backtracking state restoration uses slice truncation:
path = path[:len(path)-1]
This efficiently removes the last appended character.
The parser uses strings.Split to extract brace options and sort.Strings to maintain lexicographical order.
Worked Examples
Example 1
Input:
"{a,b}c{d,e}f"
Parsing Phase
| Position | Parsed Group |
|---|---|
{a,b} |
['a', 'b'] |
c |
['c'] |
{d,e} |
['d', 'e'] |
f |
['f'] |
Final groups:
[
['a', 'b'],
['c'],
['d', 'e'],
['f']
]
Backtracking Trace
| Step | Path | Action |
|---|---|---|
| 1 | ['a'] |
choose 'a' |
| 2 | ['a', 'c'] |
choose 'c' |
| 3 | ['a', 'c', 'd'] |
choose 'd' |
| 4 | ['a', 'c', 'd', 'f'] |
choose 'f' |
| 5 | "acdf" |
add result |
| 6 | backtrack | remove 'f' |
| 7 | ['a', 'c', 'e', 'f'] |
choose 'e', then 'f' |
| 8 | "acef" |
add result |
| 9 | continue with 'b' branch |
repeat process |
Final output:
["acdf", "acef", "bcdf", "bcef"]
Example 2
Input:
"abcd"
Parsing Phase
| Character | Parsed Group |
|---|---|
a |
['a'] |
b |
['b'] |
c |
['c'] |
d |
['d'] |
Groups:
[
['a'],
['b'],
['c'],
['d']
]
Since every group has only one option, recursion follows exactly one path:
a -> b -> c -> d
Final output:
["abcd"]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(N + K) | Parsing takes O(N), generating all output strings takes O(K) |
| Space | O(K) | Output storage dominates memory usage |
Where:
Nis the input string lengthKis the total number of characters across all generated strings
The recursive call stack depth is at most the number of character positions, which is bounded by the input length. Since every valid output must be stored, the result list dominates space complexity.
Test Cases
from typing import List
class Solution:
def expand(self, s: str) -> List[str]:
groups = []
i = 0
while i < len(s):
if s[i] == '{':
j = i
while s[j] != '}':
j += 1
options = s[i + 1:j].split(',')
options.sort()
groups.append(options)
i = j + 1
else:
groups.append([s[i]])
i += 1
result = []
path = []
def backtrack(index: int) -> None:
if index == len(groups):
result.append(''.join(path))
return
for ch in groups[index]:
path.append(ch)
backtrack(index + 1)
path.pop()
backtrack(0)
return result
sol = Solution()
assert sol.expand("{a,b}c{d,e}f") == [
"acdf",
"acef",
"bcdf",
"bcef",
] # provided example
assert sol.expand("abcd") == [
"abcd"
] # no brace groups
assert sol.expand("{a,b,c}") == [
"a",
"b",
"c",
] # single brace group
assert sol.expand("a{b,c,d}") == [
"ab",
"ac",
"ad",
] # brace group at end
assert sol.expand("{a,b}z") == [
"az",
"bz",
] # brace group at beginning
assert sol.expand("{a,b}{c,d}") == [
"ac",
"ad",
"bc",
"bd",
] # multiple consecutive groups
assert sol.expand("x") == [
"x"
] # smallest possible input
assert sol.expand("{c,b,a}") == [
"a",
"b",
"c",
] # verifies lexicographical sorting
assert sol.expand("{a,b}c") == [
"ac",
"bc",
] # mixed fixed and variable characters
Test Summary
| Test | Why |
|---|---|
"{a,b}c{d,e}f" |
Validates standard mixed expansion |
"abcd" |
Verifies no-brace handling |
"{a,b,c}" |
Tests single brace group |
"a{b,c,d}" |
Tests brace group at end |
"{a,b}z" |
Tests brace group at beginning |
"{a,b}{c,d}" |
Tests consecutive variable groups |
"x" |
Smallest valid input |
"{c,b,a}" |
Verifies sorting behavior |
"{a,b}c" |
Tests mixed fixed and variable positions |
Edge Cases
Input Without Any Braces
A common source of bugs is assuming every input contains brace expressions. For inputs like:
"abcd"
the algorithm must still work correctly.
The parser handles this naturally by treating each character as a single-option group:
[['a'], ['b'], ['c'], ['d']]
The backtracking phase then produces exactly one result.
Consecutive Brace Groups
Inputs such as:
"{a,b}{c,d}"
can expose indexing or recursion mistakes because there are no fixed characters separating groups.
The parser correctly processes each brace block independently, producing:
[['a', 'b'], ['c', 'd']]
Backtracking then explores all Cartesian product combinations.
Lexicographical Ordering
The problem requires sorted output, which can be overlooked if brace options are processed in their original order.
For example:
"{c,b,a}"
must produce:
["a", "b", "c"]
The implementation explicitly sorts every brace group's options before recursion begins. Since backtracking traverses choices in sorted order, the final results are automatically lexicographically ordered without requiring a separate sort step afterward.