Facebook Pixel

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:

  1. Single letters: Any lowercase letter by itself represents a set containing just that letter. For example, "a" represents the set {"a"}.

  2. 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)
  3. 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"}

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.

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

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:

  1. Finding the last } in the expression
  2. Finding the corresponding { that opens this brace (the nearest one to the left)
  3. 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 called s to store all unique expanded strings (automatically handles duplicates)

Main Algorithm - The dfs function:

  1. Base Case Detection:

    • First, check if there are any closing braces } in the expression using j = exp.find('}')
    • If j == -1 (no braces found), we have a fully expanded string, so add it to the set and return
  2. 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 }
  3. 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
  4. 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
  5. Final Processing:

    • After all recursive calls complete, the set s contains all unique expanded strings
    • Return the sorted list: sorted(s)

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 to dfs("ac") and dfs("ad")
    • dfs("b{c,d}") → finds inner braces, expands to dfs("bc") and dfs("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 Evaluator

Example 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 set
    • dfs("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") and dfs("kellq")
    • Both base cases add to set

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 } with find(): O(n) per recursive call
  • Finding { with rfind(): 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 creates k recursive branches
  • With m total choice points across all brace groups, this creates up to 2^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 to 2^m unique strings, each of length up to O(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:

  1. First finding the closing brace using find('}')
  2. Then searching backwards from that position using rfind('{', 0, j-1)
  3. 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

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

Load More