1087. Brace Expansion 🔒
Problem Description
You are given a string s
that represents a list of words with some positions having multiple character options. The string follows these rules:
- If a position has only one character option, it appears as a regular character
- If a position has multiple character options, they are enclosed in curly braces and separated by commas, like
{a,b,c}
Your task is to generate all possible words that can be formed by selecting one character from each position's available options.
For example:
- If
s = "a{b,c}"
, the first position must be'a'
, but the second position can be either'b'
or'c'
. This produces the words["ab", "ac"]
- If
s = "{a,b}c{d,e}"
, you can form:["acd", "ace", "bcd", "bce"]
The solution works by:
-
Parsing the string: The
convert
function processes the input string and extracts character options for each position. When it encounters curly braces, it extracts the comma-separated options inside. Regular characters outside braces are treated as single-option positions. -
Building combinations: The
dfs
function performs a depth-first search to generate all possible combinations. It iterates through each position in theitems
list, tries each available character option at that position, and recursively builds the complete words. -
Sorting the result: After generating all possible words, they are sorted lexicographically before being returned.
The algorithm systematically explores all combinations by:
- Starting at position 0 with an empty word being built
- For each position, trying each available character option
- Recursively moving to the next position
- When all positions are filled, adding the complete word to the answer list
- Backtracking to try other options
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: While we're exploring combinations, this isn't a traditional graph problem with nodes and edges. We're dealing with string manipulation and generating combinations.
Need to solve for kth smallest/largest?
- No: We need to generate all possible combinations, not find a specific kth element.
Involves Linked Lists?
- No: The problem deals with strings and character options, not linked list data structures.
Does the problem have small constraints?
- Yes: The problem involves generating all possible combinations from character options. Since we need to generate all possible words and the number of combinations grows exponentially, this typically involves small constraints (limited number of option groups and characters per group).
DFS/backtracking?
- Yes: We need to systematically explore all possible combinations by:
- Choosing one character from each position's available options
- Building complete words by making choices at each position
- Backtracking to try different options
- This is a classic backtracking pattern where we build solutions incrementally
Note on BFS: While the flowchart leads us to DFS/backtracking (which the solution uses), BFS could also solve this problem by:
- Starting with partial words at each level
- Expanding each partial word by adding all possible characters from the next position
- Building complete words level by level
- This would generate the same combinations but in a different order
Conclusion: The flowchart correctly identifies this as a backtracking problem where we need to explore all possible combinations systematically. The small constraints and need to generate all possible paths through the character options make DFS with backtracking the natural choice, though BFS would also work by building combinations level by level.
Intuition
When we look at this problem, we're essentially dealing with a decision tree where at each position in our string, we need to make a choice from the available character options. This is similar to generating all paths from root to leaves in a tree.
Think of it like filling in blanks in a word template. For "a{b,c}d{e,f}"
, we have:
- Position 0: must be
'a'
- Position 1: choose either
'b'
or'c'
- Position 2: must be
'd'
- Position 3: choose either
'e'
or'f'
The key insight is that we need to:
- First parse the string to identify what choices are available at each position
- Then systematically explore all combinations
For parsing, we need to handle two cases:
- Characters inside
{}
represent multiple options at a single position - Characters outside
{}
represent fixed characters (single option)
Once we have the options for each position stored in a list like [['a'], ['b','c'], ['d'], ['e','f']]
, generating all combinations becomes a classic backtracking problem. We:
- Start building words from position 0
- At each position, try each available character option
- Move to the next position with our partial word
- When we've filled all positions, we have a complete word
- Backtrack to try other options
This naturally forms a recursive structure where each recursive call handles one position, and the branching factor at each level depends on how many options that position has. The recursion depth equals the number of positions in our parsed structure.
The backtracking approach ensures we explore every possible combination exactly once, and sorting the final result gives us the lexicographical ordering required.
Learn more about Breadth-First Search and Backtracking patterns.
Solution Approach
The solution uses a two-phase approach: parsing and generation.
Phase 1: Parsing the Input String
The convert
function recursively parses the input string to extract character options for each position:
- When encountering
'{'
, it finds the matching'}'
and extracts the comma-separated options inside - For regular characters (not in braces), it treats them as single-option positions
- The function recursively processes the remaining string after each extraction
- Results are stored in the
items
list, where each element represents the available characters for that position
For example, "a{b,c}d"
becomes [['a'], ['b', 'c'], ['d']]
.
Phase 2: Generating All Combinations with DFS
The dfs
function performs depth-first search with backtracking to generate all possible words:
def dfs(i, t):
if i == len(items): # Base case: all positions filled
ans.append(''.join(t))
return
for c in items[i]: # Try each option at current position
t.append(c) # Make choice
dfs(i + 1, t) # Recurse to next position
t.pop() # Backtrack
The algorithm:
- Starts with position
i = 0
and empty listt
to build the current word - At each position, iterates through all available character options
- Appends the chosen character to the current word being built
- Recursively moves to the next position
- After exploring all paths from that choice, backtracks by removing the character
- When
i
reaches the length ofitems
, a complete word has been formed
Time Complexity: O(N * M^K)
where N
is the length of the input string, K
is the number of option groups, and M
is the maximum number of options in any group. We need O(N)
to parse and O(M^K)
to generate all combinations.
Space Complexity: O(K * M)
for storing the parsed options plus O(K)
for the recursion stack depth.
The final step sorts the generated words lexicographically using Python's built-in sort, which adds O(P * log P * L)
time where P
is the total number of words generated and L
is the average word length.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with s = "{a,b}c{d,e}"
.
Step 1: Parse the input string
Starting with convert(s, 0, items)
:
- Position 0: We see
'{'
, so we find the matching'}'
at position 5 - Extract content between braces:
"a,b"
- Split by comma to get
['a', 'b']
and add to items - Recursively process from position 6
Continue parsing:
- Position 6: We see
'c'
(regular character) - Add
['c']
to items - Recursively process from position 7
Continue parsing:
- Position 7: We see
'{'
, find matching'}'
at position 11 - Extract
"d,e"
, split to get['d', 'e']
and add to items - Position 12 is end of string, so we're done
Result: items = [['a', 'b'], ['c'], ['d', 'e']]
Step 2: Generate all combinations using DFS
Starting with dfs(0, [])
:
dfs(0, []) ├─ Try 'a' from items[0] │ └─ dfs(1, ['a']) │ └─ Try 'c' from items[1] │ └─ dfs(2, ['a','c']) │ ├─ Try 'd' from items[2] │ │ └─ dfs(3, ['a','c','d']) │ │ └─ i==3, add "acd" to ans │ └─ Try 'e' from items[2] │ └─ dfs(3, ['a','c','e']) │ └─ i==3, add "ace" to ans └─ Try 'b' from items[0] └─ dfs(1, ['b']) └─ Try 'c' from items[1] └─ dfs(2, ['b','c']) ├─ Try 'd' from items[2] │ └─ dfs(3, ['b','c','d']) │ └─ i==3, add "bcd" to ans └─ Try 'e' from items[2] └─ dfs(3, ['b','c','e']) └─ i==3, add "bce" to ans
Generated words: ["acd", "ace", "bcd", "bce"]
Step 3: Sort the result
The words are already in lexicographical order, so the final answer is: ["acd", "ace", "bcd", "bce"]
The solution efficiently explores all 2×1×2 = 4 possible combinations by making choices at each position with multiple options.
Solution Implementation
1class Solution:
2 def expand(self, s: str) -> List[str]:
3 """
4 Expands a string with bracketed options into all possible combinations.
5 Example: "a{b,c}d{e,f}" -> ["abde", "abdf", "acde", "acdf"]
6 """
7
8 def parse_string(s: str) -> None:
9 """
10 Recursively parse the input string to extract components.
11 Components are either single characters or options within braces.
12 """
13 if not s:
14 return
15
16 if s[0] == '{':
17 # Find the closing brace
18 closing_brace_idx = s.find('}')
19 # Extract options between braces and split by comma
20 options = s[1:closing_brace_idx].split(',')
21 components.append(options)
22 # Continue parsing the rest of the string
23 parse_string(s[closing_brace_idx + 1:])
24 else:
25 # Find the next opening brace if exists
26 next_brace_idx = s.find('{')
27 if next_brace_idx != -1:
28 # Extract characters before the next brace
29 # Each character is treated as a single-option component
30 for char in s[:next_brace_idx]:
31 components.append([char])
32 # Continue parsing from the brace
33 parse_string(s[next_brace_idx:])
34 else:
35 # No more braces, treat remaining characters as single-option components
36 for char in s:
37 components.append([char])
38
39 def generate_combinations(index: int, current_path: List[str]) -> None:
40 """
41 Use DFS to generate all possible combinations by selecting
42 one option from each component.
43 """
44 # Base case: reached the end of components
45 if index == len(components):
46 result.append(''.join(current_path))
47 return
48
49 # Try each option in the current component
50 for option in components[index]:
51 current_path.append(option)
52 generate_combinations(index + 1, current_path)
53 current_path.pop() # Backtrack
54
55 # Initialize list to store parsed components
56 components = []
57 parse_string(s)
58
59 # Initialize list to store all generated combinations
60 result = []
61 generate_combinations(0, [])
62
63 # Sort the result lexicographically
64 result.sort()
65 return result
66
1class Solution {
2 // List to store all expanded string combinations
3 private List<String> ans;
4 // List to store parsed groups of characters/strings
5 private List<String[]> items;
6
7 /**
8 * Expands a string containing optional characters in braces
9 * Example: "{a,b}c{d,e}f" -> ["acdf", "acef", "bcdf", "bcef"]
10 * @param s Input string with optional characters in braces
11 * @return Array of all possible expanded strings in lexicographical order
12 */
13 public String[] expand(String s) {
14 ans = new ArrayList<>();
15 items = new ArrayList<>();
16
17 // Parse the input string into groups
18 convert(s);
19
20 // Generate all combinations using DFS
21 dfs(0, new ArrayList<>());
22
23 // Sort results lexicographically
24 Collections.sort(ans);
25
26 return ans.toArray(new String[0]);
27 }
28
29 /**
30 * Recursively parses the input string and extracts character groups
31 * Groups within braces are split by comma, single characters are stored as is
32 * @param s The string to parse
33 */
34 private void convert(String s) {
35 // Base case: empty string
36 if ("".equals(s)) {
37 return;
38 }
39
40 // Case 1: String starts with a brace group
41 if (s.charAt(0) == '{') {
42 // Find the closing brace
43 int closingBraceIndex = s.indexOf("}");
44
45 // Extract and split the content within braces
46 String[] options = s.substring(1, closingBraceIndex).split(",");
47 items.add(options);
48
49 // Recursively process the remaining string
50 convert(s.substring(closingBraceIndex + 1));
51 }
52 // Case 2: String starts with regular characters
53 else {
54 // Find the next opening brace (if any)
55 int openingBraceIndex = s.indexOf("{");
56
57 if (openingBraceIndex != -1) {
58 // Extract characters before the next brace group
59 String chars = s.substring(0, openingBraceIndex);
60 items.add(chars.split(","));
61
62 // Recursively process from the brace onwards
63 convert(s.substring(openingBraceIndex));
64 } else {
65 // No more braces, add remaining characters
66 items.add(s.split(","));
67 }
68 }
69 }
70
71 /**
72 * Depth-first search to generate all possible combinations
73 * @param index Current position in the items list
74 * @param currentPath Current combination being built
75 */
76 private void dfs(int index, List<String> currentPath) {
77 // Base case: reached the end of items, add complete combination
78 if (index == items.size()) {
79 ans.add(String.join("", currentPath));
80 return;
81 }
82
83 // Try each option at the current position
84 for (String character : items.get(index)) {
85 // Add current character to path
86 currentPath.add(character);
87
88 // Recursively explore with next position
89 dfs(index + 1, currentPath);
90
91 // Backtrack: remove the character for next iteration
92 currentPath.remove(currentPath.size() - 1);
93 }
94 }
95}
96
1class Solution {
2private:
3 // Vector to store all expanded string combinations
4 vector<string> ans;
5 // Vector to store parsed groups of characters/strings
6 vector<vector<string>> items;
7
8 /**
9 * Recursively parses the input string and extracts character groups
10 * Groups within braces are split by comma, single characters are stored as is
11 * @param s The string to parse
12 */
13 void convert(string s) {
14 // Base case: empty string
15 if (s.empty()) {
16 return;
17 }
18
19 // Case 1: String starts with a brace group
20 if (s[0] == '{') {
21 // Find the closing brace
22 size_t closingBraceIndex = s.find('}');
23
24 // Extract and split the content within braces
25 string content = s.substr(1, closingBraceIndex - 1);
26 vector<string> options;
27 stringstream ss(content);
28 string option;
29 while (getline(ss, option, ',')) {
30 options.push_back(option);
31 }
32 items.push_back(options);
33
34 // Recursively process the remaining string
35 convert(s.substr(closingBraceIndex + 1));
36 }
37 // Case 2: String starts with regular characters
38 else {
39 // Find the next opening brace (if any)
40 size_t openingBraceIndex = s.find('{');
41
42 if (openingBraceIndex != string::npos) {
43 // Extract characters before the next brace group
44 string chars = s.substr(0, openingBraceIndex);
45 vector<string> charVector;
46 for (char c : chars) {
47 charVector.push_back(string(1, c));
48 }
49 items.push_back(charVector);
50
51 // Recursively process from the brace onwards
52 convert(s.substr(openingBraceIndex));
53 } else {
54 // No more braces, add remaining characters
55 vector<string> charVector;
56 for (char c : s) {
57 charVector.push_back(string(1, c));
58 }
59 items.push_back(charVector);
60 }
61 }
62 }
63
64 /**
65 * Depth-first search to generate all possible combinations
66 * @param index Current position in the items list
67 * @param currentPath Current combination being built
68 */
69 void dfs(int index, string currentPath) {
70 // Base case: reached the end of items, add complete combination
71 if (index == items.size()) {
72 ans.push_back(currentPath);
73 return;
74 }
75
76 // Try each option at the current position
77 for (const string& character : items[index]) {
78 // Recursively explore with current character added to path
79 dfs(index + 1, currentPath + character);
80 }
81 }
82
83public:
84 /**
85 * Expands a string containing optional characters in braces
86 * Example: "{a,b}c{d,e}f" -> ["acdf", "acef", "bcdf", "bcef"]
87 * @param s Input string with optional characters in braces
88 * @return Vector of all possible expanded strings in lexicographical order
89 */
90 vector<string> expand(string s) {
91 ans.clear();
92 items.clear();
93
94 // Parse the input string into groups
95 convert(s);
96
97 // Generate all combinations using DFS
98 dfs(0, "");
99
100 // Sort results lexicographically
101 sort(ans.begin(), ans.end());
102
103 return ans;
104 }
105};
106
1// List to store all expanded string combinations
2let ans: string[];
3// List to store parsed groups of characters/strings
4let items: string[][];
5
6/**
7 * Expands a string containing optional characters in braces
8 * Example: "{a,b}c{d,e}f" -> ["acdf", "acef", "bcdf", "bcef"]
9 * @param s Input string with optional characters in braces
10 * @return Array of all possible expanded strings in lexicographical order
11 */
12function expand(s: string): string[] {
13 ans = [];
14 items = [];
15
16 // Parse the input string into groups
17 convert(s);
18
19 // Generate all combinations using DFS
20 dfs(0, []);
21
22 // Sort results lexicographically
23 ans.sort();
24
25 return ans;
26}
27
28/**
29 * Recursively parses the input string and extracts character groups
30 * Groups within braces are split by comma, single characters are stored as is
31 * @param s The string to parse
32 */
33function convert(s: string): void {
34 // Base case: empty string
35 if (s === "") {
36 return;
37 }
38
39 // Case 1: String starts with a brace group
40 if (s.charAt(0) === '{') {
41 // Find the closing brace
42 const closingBraceIndex = s.indexOf("}");
43
44 // Extract and split the content within braces
45 const options = s.substring(1, closingBraceIndex).split(",");
46 items.push(options);
47
48 // Recursively process the remaining string
49 convert(s.substring(closingBraceIndex + 1));
50 }
51 // Case 2: String starts with regular characters
52 else {
53 // Find the next opening brace (if any)
54 const openingBraceIndex = s.indexOf("{");
55
56 if (openingBraceIndex !== -1) {
57 // Extract characters before the next brace group
58 const chars = s.substring(0, openingBraceIndex);
59 // Split each character individually for regular characters
60 const charArray = chars.split("").map(c => c);
61 for (const char of charArray) {
62 items.push([char]);
63 }
64
65 // Recursively process from the brace onwards
66 convert(s.substring(openingBraceIndex));
67 } else {
68 // No more braces, add remaining characters individually
69 const charArray = s.split("").map(c => c);
70 for (const char of charArray) {
71 items.push([char]);
72 }
73 }
74 }
75}
76
77/**
78 * Depth-first search to generate all possible combinations
79 * @param index Current position in the items list
80 * @param currentPath Current combination being built
81 */
82function dfs(index: number, currentPath: string[]): void {
83 // Base case: reached the end of items, add complete combination
84 if (index === items.length) {
85 ans.push(currentPath.join(""));
86 return;
87 }
88
89 // Try each option at the current position
90 for (const character of items[index]) {
91 // Add current character to path
92 currentPath.push(character);
93
94 // Recursively explore with next position
95 dfs(index + 1, currentPath);
96
97 // Backtrack: remove the character for next iteration
98 currentPath.pop();
99 }
100}
101
Time and Space Complexity
Time Complexity: O(n + m * k^m)
where:
n
is the length of the input stringm
is the number of groups (items) after parsingk
is the maximum number of options in any single group
The analysis breaks down into two main phases:
-
Parsing phase (
convert
function):O(n)
- The function traverses the entire string once
- String operations like
find()
andsplit()
are performed, but each character is processed at most once
-
DFS generation phase (
dfs
function):O(k^m)
- The DFS explores all possible combinations
- At each level
i
, we iterate through all options initems[i]
- The total number of combinations is the product of the number of options in each group
- In the worst case, if each group has
k
options and there arem
groups, we generatek^m
combinations - Each combination takes
O(m)
time to build (appending and popping from the list)
-
Sorting phase:
O(k^m * m * log(k^m))
- We sort
k^m
strings, each of length at mostm
- String comparison takes
O(m)
time - Total sorting time:
O(k^m * m * log(k^m))
- We sort
Overall time complexity: O(n + k^m * m * log(k^m))
Space Complexity: O(n + m * k^m)
where:
-
Items array:
O(n)
- Stores the parsed groups, which collectively contain all characters from the input
-
Recursion stack:
O(m)
- Maximum recursion depth equals the number of groups
-
Temporary array
t
in DFS:O(m)
- Stores the current combination being built
-
Answer array:
O(k^m * m)
- Stores all generated combinations
- At most
k^m
strings, each of length at mostm
The dominant space factor is the answer array, giving us O(n + m * k^m)
total space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Parsing of Consecutive Single Characters
A common mistake is treating consecutive single characters outside of braces as a single component instead of separate positions. For example, with input "abc{d,e}"
, an incorrect implementation might parse it as [['abc'], ['d', 'e']]
instead of the correct [['a'], ['b'], ['c'], ['d', 'e']]
.
Why this happens: When encountering regular characters, developers often extract them as a single string chunk rather than iterating through each character individually.
Incorrect approach:
# Wrong: treats "abc" as one component if next_brace_idx != -1: components.append([s[:next_brace_idx]]) # Bug: treats entire substring as one option
Correct approach:
# Right: treats each character as a separate position if next_brace_idx != -1: for char in s[:next_brace_idx]: components.append([char]) # Each character is its own position
2. Memory Issues with String Concatenation in DFS
Building strings by concatenation during recursion (current_word += option
) creates new string objects at each step, leading to unnecessary memory overhead and potential stack overflow for large inputs.
Problematic pattern:
def dfs(index, current_word): # String passed by value
if index == len(components):
result.append(current_word)
return
for option in components[index]:
dfs(index + 1, current_word + option) # Creates new string each time
Better approach using list:
def dfs(index, current_path): # List passed by reference
if index == len(components):
result.append(''.join(current_path)) # Join only at the end
return
for option in components[index]:
current_path.append(option)
dfs(index + 1, current_path)
current_path.pop() # Proper backtracking
3. Missing Edge Cases in Parsing
Failing to handle edge cases like empty braces {}
, consecutive braces {a}{b}
, or braces at the beginning/end of the string can cause index errors or incorrect results.
Common oversights:
- Not checking for empty input string
- Assuming there's always content between or around braces
- Not handling malformed input gracefully
Robust parsing checks:
def parse_string(s: str) -> None:
if not s: # Handle empty string
return
if s[0] == '{':
closing_brace_idx = s.find('}')
if closing_brace_idx == -1: # Handle unclosed brace
raise ValueError("Unclosed brace")
options = s[1:closing_brace_idx].split(',')
if not options or options == ['']: # Handle empty braces
return # Skip empty option groups
components.append(options)
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
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!