Facebook Pixel

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:

  1. 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.

  2. Building combinations: The dfs function performs a depth-first search to generate all possible combinations. It iterates through each position in the items list, tries each available character option at that position, and recursively builds the complete words.

  3. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. First parse the string to identify what choices are available at each position
  2. 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:

  1. Starts with position i = 0 and empty list t to build the current word
  2. At each position, iterates through all available character options
  3. Appends the chosen character to the current word being built
  4. Recursively moves to the next position
  5. After exploring all paths from that choice, backtracks by removing the character
  6. When i reaches the length of items, 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 Evaluator

Example 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 string
  • m is the number of groups (items) after parsing
  • k is the maximum number of options in any single group

The analysis breaks down into two main phases:

  1. Parsing phase (convert function): O(n)

    • The function traverses the entire string once
    • String operations like find() and split() are performed, but each character is processed at most once
  2. DFS generation phase (dfs function): O(k^m)

    • The DFS explores all possible combinations
    • At each level i, we iterate through all options in items[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 are m groups, we generate k^m combinations
    • Each combination takes O(m) time to build (appending and popping from the list)
  3. Sorting phase: O(k^m * m * log(k^m))

    • We sort k^m strings, each of length at most m
    • String comparison takes O(m) time
    • Total sorting time: O(k^m * m * log(k^m))

Overall time complexity: O(n + k^m * m * log(k^m))

Space Complexity: O(n + m * k^m) where:

  1. Items array: O(n)

    • Stores the parsed groups, which collectively contain all characters from the input
  2. Recursion stack: O(m)

    • Maximum recursion depth equals the number of groups
  3. Temporary array t in DFS: O(m)

    • Stores the current combination being built
  4. Answer array: O(k^m * m)

    • Stores all generated combinations
    • At most k^m strings, each of length at most m

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)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More