1096. Brace Expansion II
Problem Description
This problem asks you to parse and evaluate a special grammar that represents sets of lowercase words. Given an expression string, you need to return all possible words that can be generated from it, sorted in lexicographical order.
The grammar has three rules:
-
Single letters: Any lowercase letter by itself represents a set containing just that letter. For example,
"a"
represents the set{"a"}
. -
Union with braces and commas: When expressions are enclosed in braces and separated by commas like
{expr1,expr2,...}
, this represents the union of all the sets. For example:"{a,b,c}"
represents{"a", "b", "c"}
"{{a,b},{b,c}}"
represents{"a", "b", "c"}
(duplicates are removed in the union)
-
Concatenation: When expressions are placed next to each other, they are concatenated. This means taking every possible combination where you pick one word from the first set and one from the second set, then concatenate them. For example:
"{a,b}{c,d}"
represents all combinations:{"ac", "ad", "bc", "bd"}
"a{b,c}{d,e}f{g,h}"
breaks down as:- Start with
"a"
- Concatenate with either
"b"
or"c"
- Concatenate with either
"d"
or"e"
- Concatenate with
"f"
- Concatenate with either
"g"
or"h"
- This gives 8 total combinations:
{"abdfg", "abdfh", "abefg", "abefh", "acdfg", "acdfh", "acefg", "acefh"}
- Start with
The solution uses a recursive approach that finds the innermost brace pairs (those without nested braces inside), expands them by creating all possible combinations with the surrounding text, and continues this process until no braces remain. The final result is a sorted list of all unique words that can be generated from the expression.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: While not explicitly a graph problem, we can model this as a graph where each partial expansion state is a node, and edges represent expansion operations. We're exploring all possible expansions from the initial expression.
Is it a tree?
- No: The expansion process doesn't form a tree structure since different expansion paths can lead to the same result (we need to handle duplicates).
Is the problem related to directed acyclic graphs?
- No: The problem isn't about directed acyclic graphs or dependencies.
Is the problem related to shortest paths?
- No: We're not finding shortest paths; we need to explore all possible expansions.
Does the problem involve connectivity?
- No: We're not checking if nodes are connected or finding connected components.
Does the problem have small constraints?
- No: The expression can be arbitrarily complex with nested braces, so we can't rely on small constraint assumptions.
BFS
- Yes: We arrive at BFS as the recommended approach.
Why BFS makes sense here:
- We need to systematically expand expressions level by level (innermost braces first)
- Each expansion creates new states that need to be processed
- BFS ensures we process all expansions at the current level before moving deeper
- We can use a queue to manage the states to be processed
- The solution's recursive approach effectively performs a breadth-first expansion by finding and expanding the innermost braces first
Conclusion: The flowchart suggests using BFS for systematically exploring all possible expansions of the brace expression, processing each level of expansion before moving to the next.
Intuition
The key insight is to process the expression from the inside out, similar to how we evaluate mathematical expressions with parentheses. When we see nested braces like "a{b,{c,d}e}"
, we need to first resolve the innermost braces before handling the outer ones.
Think of the expression as layers of an onion. We peel away one layer at a time by finding the innermost brace pair (a pair with no other braces inside it) and expanding it. For example, in "{a,b}{c,d}"
, both brace pairs are at the same "depth" and contain no nested braces, so we can expand either one first.
The recursive approach works by:
- Finding the last
}
in the expression - Finding the corresponding
{
that opens this brace (the nearest one to the left) - This guarantees we've found an innermost brace pair with no nested braces inside
Once we identify an innermost brace pair like {a,b,c}
, we expand it by creating multiple versions of the expression - one for each option inside the braces. If our expression is "x{a,b,c}y"
, we create three new expressions: "xay"
, "xby"
, and "xcy"
.
We continue this process recursively on each generated expression until no braces remain. At that point, we have a simple concatenated string that we add to our result set. Using a set automatically handles duplicates for us, which is important since different expansion paths might lead to the same final string.
The beauty of this approach is its simplicity - by always working with the innermost braces first, we avoid the complexity of parsing nested structures. Each recursive call either finds another brace pair to expand or adds a final result to our set. The sorted return ensures we meet the problem's requirement for lexicographical ordering.
Learn more about Stack, Breadth-First Search and Backtracking patterns.
Solution Approach
The implementation uses a recursive depth-first search approach with a set to store unique results. Let's walk through the key components:
Data Structure:
- A
set
calleds
to store all unique expanded strings (automatically handles duplicates)
Main Algorithm - The dfs
function:
-
Base Case Detection:
- First, check if there are any closing braces
}
in the expression usingj = exp.find('}')
- If
j == -1
(no braces found), we have a fully expanded string, so add it to the set and return
- First, check if there are any closing braces
-
Finding Innermost Braces:
- Find the first closing brace position:
j = exp.find('}')
- Find the corresponding opening brace by searching backwards from position
j-1
:i = exp.rfind('{', 0, j - 1)
- This guarantees we found an innermost pair since
rfind
searches from right to left, finding the closest{
to our}
- Find the first closing brace position:
-
String Decomposition:
- Split the expression into three parts:
a = exp[:i]
- everything before the opening brace- The content between braces:
exp[i + 1 : j]
c = exp[j + 1:]
- everything after the closing brace
- Split the expression into three parts:
-
Expansion Process:
- Split the content between braces by commas:
exp[i + 1 : j].split(',')
- For each option
b
in the comma-separated list:- Create a new expression:
a + b + c
- Recursively call
dfs
on this new expression
- Create a new expression:
- Split the content between braces by commas:
-
Final Processing:
- After all recursive calls complete, the set
s
contains all unique expanded strings - Return the sorted list:
sorted(s)
- After all recursive calls complete, the set
Example Walkthrough with "{a,b}{c,d}"
:
- Initial call:
dfs("{a,b}{c,d}")
- Finds
j=4
(first}
),i=0
(corresponding{
) - Splits into:
a=""
, options=["a","b"]
,c="{c,d}"
- Creates two recursive calls:
dfs("a{c,d}")
→ finds inner braces, expands todfs("ac")
anddfs("ad")
dfs("b{c,d}")
→ finds inner braces, expands todfs("bc")
anddfs("bd")
- Base cases add
"ac"
,"ad"
,"bc"
,"bd"
to the set - Returns
["ac", "ad", "bc", "bd"]
after sorting
The elegance of this solution lies in its simplicity - by always processing the innermost braces first, we avoid complex parsing logic and naturally handle all nested structures through recursion.
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 trace through the expression "k{a,e}l{l{p,q}}"
step by step to see how the recursive solution works:
Initial Call: dfs("k{a,e}l{l{p,q}}")
- Find last
}
: position 14 - Find matching
{
: position 10 (using rfind from position 13) - This gives us the innermost brace pair
{p,q}
- Split:
a = "k{a,e}l{l"
, options =["p", "q"]
,c = "}"
- Create two recursive calls:
dfs("k{a,e}l{lp}")
dfs("k{a,e}l{lq}")
First Branch: dfs("k{a,e}l{lp}")
- Find last
}
: position 9 - Find matching
{
: position 8 - This identifies
{lp}
as innermost - Split:
a = "k{a,e}l"
, options =["lp"]
,c = ""
- Create one recursive call:
dfs("k{a,e}llp")
Continuing: dfs("k{a,e}llp")
- Find last
}
: position 5 - Find matching
{
: position 1 - This identifies
{a,e}
as innermost - Split:
a = "k"
, options =["a", "e"]
,c = "llp"
- Create two recursive calls:
dfs("kallp")
- no braces, add to setdfs("kellp")
- no braces, add to set
Second Branch: dfs("k{a,e}l{lq}")
- Following similar logic:
- Expands
{lq}
→dfs("k{a,e}llq")
- Expands
{a,e}
→dfs("kallq")
anddfs("kellq")
- Both base cases add to set
- Expands
Final Result:
- Set contains:
{"kallp", "kellp", "kallq", "kellq"}
- After sorting:
["kallp", "kallq", "kellp", "kellq"]
The key insight is that by always finding the last }
and its matching {
, we guarantee we're working with innermost braces that contain no nested structures, making the expansion straightforward.
Solution Implementation
1class Solution:
2 def braceExpansionII(self, expression: str) -> List[str]:
3 def dfs(current_expression):
4 # Find the first closing brace
5 closing_brace_index = current_expression.find('}')
6
7 # Base case: no more braces to expand
8 if closing_brace_index == -1:
9 result_set.add(current_expression)
10 return
11
12 # Find the matching opening brace for the first closing brace
13 # Search backwards from just before the closing brace
14 opening_brace_index = current_expression.rfind('{', 0, closing_brace_index - 1)
15
16 # Extract parts: before the brace, inside the brace, and after the brace
17 prefix = current_expression[:opening_brace_index]
18 suffix = current_expression[closing_brace_index + 1:]
19 brace_content = current_expression[opening_brace_index + 1:closing_brace_index]
20
21 # Expand each option within the braces
22 for option in brace_content.split(','):
23 # Recursively process the expression with this option substituted
24 dfs(prefix + option + suffix)
25
26 # Initialize result set to store unique expanded strings
27 result_set = set()
28
29 # Start the depth-first search expansion
30 dfs(expression)
31
32 # Return sorted list of all unique expanded strings
33 return sorted(result_set)
34
1class Solution {
2 // TreeSet to store unique results in sorted order
3 private TreeSet<String> resultSet = new TreeSet<>();
4
5 /**
6 * Expands the brace expression and returns all possible combinations.
7 * @param expression - the brace expression to expand
8 * @return List of all possible strings after expansion, sorted lexicographically
9 */
10 public List<String> braceExpansionII(String expression) {
11 // Start recursive expansion
12 expandExpression(expression);
13 // Convert TreeSet to ArrayList and return
14 return new ArrayList<>(resultSet);
15 }
16
17 /**
18 * Recursively expands the expression by processing innermost braces first.
19 * @param expression - current expression to process
20 */
21 private void expandExpression(String expression) {
22 // Find the first closing brace
23 int closingBraceIndex = expression.indexOf('}');
24
25 // Base case: no braces left, add the final string to result set
26 if (closingBraceIndex == -1) {
27 resultSet.add(expression);
28 return;
29 }
30
31 // Find the matching opening brace (the last '{' before the closing brace)
32 int openingBraceIndex = expression.lastIndexOf('{', closingBraceIndex);
33
34 // Extract parts: prefix before '{', content between braces, suffix after '}'
35 String prefix = expression.substring(0, openingBraceIndex);
36 String suffix = expression.substring(closingBraceIndex + 1);
37 String braceContent = expression.substring(openingBraceIndex + 1, closingBraceIndex);
38
39 // Split the content by comma and recursively process each option
40 for (String option : braceContent.split(",")) {
41 // Construct new expression by replacing {content} with one option
42 String newExpression = prefix + option + suffix;
43 // Recursively expand the new expression
44 expandExpression(newExpression);
45 }
46 }
47}
48
1class Solution {
2public:
3 vector<string> braceExpansionII(string expression) {
4 // Recursively expand the expression and store results in a set
5 expandExpression(expression);
6
7 // Convert set to vector and return (set automatically sorts and removes duplicates)
8 return vector<string>(resultSet.begin(), resultSet.end());
9 }
10
11private:
12 set<string> resultSet; // Store unique expanded strings in sorted order
13
14 void expandExpression(string expression) {
15 // Find the first closing brace in the expression
16 int closeBracePos = expression.find_first_of('}');
17
18 // Base case: no more braces to expand, add the final string to result set
19 if (closeBracePos == string::npos) {
20 resultSet.insert(expression);
21 return;
22 }
23
24 // Find the matching opening brace for the first closing brace
25 // Search backwards from the closing brace position
26 int openBracePos = expression.rfind('{', closeBracePos);
27
28 // Extract three parts of the expression:
29 // 1. prefix: everything before the opening brace
30 string prefix = expression.substr(0, openBracePos);
31
32 // 2. suffix: everything after the closing brace
33 string suffix = expression.substr(closeBracePos + 1);
34
35 // 3. options: content between the braces (comma-separated values)
36 string bracedContent = expression.substr(openBracePos + 1, closeBracePos - openBracePos - 1);
37
38 // Parse comma-separated options within the braces
39 stringstream optionStream(bracedContent);
40 string option;
41
42 // For each option, recursively expand the expression
43 while (getline(optionStream, option, ',')) {
44 // Combine prefix + current option + suffix and recursively expand
45 expandExpression(prefix + option + suffix);
46 }
47 }
48};
49
1/**
2 * Expands a brace expression and returns all possible combinations in sorted order.
3 * Handles nested braces and comma-separated options within braces.
4 *
5 * @param expression - The input string containing brace expressions
6 * @returns Array of all possible expanded strings in lexicographical order
7 */
8function braceExpansionII(expression: string): string[] {
9 // Set to store unique expanded results
10 const expandedResults: Set<string> = new Set<string>();
11
12 /**
13 * Recursively expands the expression by finding and processing brace pairs.
14 * Works from innermost braces outward.
15 *
16 * @param currentExpression - The current expression being processed
17 */
18 const expandExpression = (currentExpression: string): void => {
19 // Find the first closing brace
20 const closingBraceIndex: number = currentExpression.indexOf('}');
21
22 // Base case: no more braces to expand
23 if (closingBraceIndex === -1) {
24 expandedResults.add(currentExpression);
25 return;
26 }
27
28 // Find the matching opening brace (the last '{' before the closing brace)
29 const openingBraceIndex: number = currentExpression.lastIndexOf('{', closingBraceIndex);
30
31 // Extract parts: before brace, inside brace, after brace
32 const prefixPart: string = currentExpression.substring(0, openingBraceIndex);
33 const suffixPart: string = currentExpression.substring(closingBraceIndex + 1);
34 const braceContent: string = currentExpression.substring(openingBraceIndex + 1, closingBraceIndex);
35
36 // Split options by comma and recursively expand each combination
37 const options: string[] = braceContent.split(',');
38 for (const option of options) {
39 expandExpression(prefixPart + option + suffixPart);
40 }
41 };
42
43 // Start the recursive expansion
44 expandExpression(expression);
45
46 // Convert set to array and sort lexicographically
47 return Array.from(expandedResults).sort();
48}
49
Time and Space Complexity
Time Complexity: O(n * 2^m * m)
where n
is the length of the expression and m
is the number of choice points (comma-separated options within braces).
The algorithm recursively expands each brace group from innermost to outermost. In the worst case:
- Finding
}
withfind()
:O(n)
per recursive call - Finding
{
withrfind()
:O(n)
per recursive call - String slicing and concatenation
a + b + c
:O(n)
per recursive call - Splitting by comma:
O(n)
per recursive call - Each brace group with
k
comma-separated options createsk
recursive branches - With
m
total choice points across all brace groups, this creates up to2^m
total recursive paths (product of all choices) - Each recursive call does
O(n)
work - Total:
O(n * 2^m * m)
considering the depth of recursion
Space Complexity: O(n * 2^m)
where n
is the length of the expression and m
is the number of choice points.
The space is consumed by:
- Recursion stack depth:
O(m)
in the worst case (one level per brace group) - String storage in each recursive call:
O(n)
per call - Set
s
storing unique results: up to2^m
unique strings, each of length up toO(n)
- Total:
O(n * 2^m)
dominated by the storage of all possible expanded strings in the set
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Brace Matching with Nested Structures
The most critical pitfall is incorrectly matching opening and closing braces when there are nested structures. Consider the expression "{{a,b},{c,d}}e"
:
Wrong Approach:
# If we naively find the first '{' and first '}' opening = expression.find('{') # finds position 0 closing = expression.find('}') # finds position 6 # This would incorrectly pair the outermost '{' with the first inner '}'
Why the Current Solution Works: The solution cleverly avoids this by:
- First finding the closing brace using
find('}')
- Then searching backwards from that position using
rfind('{', 0, j-1)
- This ensures we always get the innermost brace pair
2. Handling Adjacent Concatenations Without Braces
Another pitfall occurs when trying to handle expressions like "ab{c,d}ef"
where letters are directly concatenated without braces.
Potential Issue: If the algorithm tried to process concatenations separately from brace expansion, it would need complex logic to determine where one "unit" ends and another begins.
How It's Avoided:
The current solution treats everything outside braces as literal strings that get carried along (prefix
and suffix
), naturally handling concatenation through the recursive structure.
3. Memory Issues with Large Expressions
For expressions with many nested braces and options, the set can grow exponentially.
Example Problem:
# This expression generates 2^10 = 1024 unique strings expression = "{a,b}{c,d}{e,f}{g,h}{i,j}{k,l}{m,n}{o,p}{q,r}{s,t}"
Mitigation Strategy: While the current solution handles this correctly, for very large inputs you might need to:
- Check expression length limits early
- Consider iterative approaches with explicit stack management
- Monitor memory usage during recursion
4. Edge Cases with Empty Options
A subtle pitfall is handling expressions with empty options or consecutive commas:
Problematic Input:
expression = "{a,,b}" # Empty string between commas expression = "{,a,b}" # Empty string at start
Solution Enhancement:
def dfs(current_expression):
closing_brace_index = current_expression.find('}')
if closing_brace_index == -1:
# Only add non-empty results
if current_expression: # Add this check
result_set.add(current_expression)
return
opening_brace_index = current_expression.rfind('{', 0, closing_brace_index - 1)
prefix = current_expression[:opening_brace_index]
suffix = current_expression[closing_brace_index + 1:]
brace_content = current_expression[opening_brace_index + 1:closing_brace_index]
for option in brace_content.split(','):
# Skip empty options if needed
if option or (not prefix and not suffix): # Allow empty only if it's the entire result
dfs(prefix + option + suffix)
5. Assumption About Input Validity
The solution assumes well-formed input (balanced braces, proper comma placement). For production code, validation would be important:
Adding Input Validation:
def validate_expression(expression):
# Check for balanced braces
brace_count = 0
for char in expression:
if char == '{':
brace_count += 1
elif char == '}':
brace_count -= 1
if brace_count < 0:
return False
return brace_count == 0
Given a sorted array of integers and an integer called target, find the element that
equals to the target and return its index. Select the correct code that fills the
___
in the given code snippet.
1def binary_search(arr, target):
2 left, right = 0, len(arr) - 1
3 while left ___ right:
4 mid = (left + right) // 2
5 if arr[mid] == target:
6 return mid
7 if arr[mid] < target:
8 ___ = mid + 1
9 else:
10 ___ = mid - 1
11 return -1
12
1public static int binarySearch(int[] arr, int target) {
2 int left = 0;
3 int right = arr.length - 1;
4
5 while (left ___ right) {
6 int mid = left + (right - left) / 2;
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
1function binarySearch(arr, target) {
2 let left = 0;
3 let right = arr.length - 1;
4
5 while (left ___ right) {
6 let mid = left + Math.trunc((right - left) / 2);
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
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
Want a Structured Path to Master System Design Too? Don’t Miss This!