LeetCode 1096 - Brace Expansion II
The problem asks us to interpret a string expression representing a set of words generated according to a specific grammar and return all distinct words sorted in lexicographical order. The expression can contain lowercase letters, curly braces {}, and commas ,.
Difficulty: 🔴 Hard
Topics: Hash Table, String, Backtracking, Stack, Breadth-First Search, Sorting
Solution
Problem Understanding
The problem asks us to interpret a string expression representing a set of words generated according to a specific grammar and return all distinct words sorted in lexicographical order. The expression can contain lowercase letters, curly braces {}, and commas ,. The rules of the grammar are:
- A single lowercase letter represents a set containing only that letter.
- A comma-separated list inside
{}represents a union of all expressions within the braces. - Concatenation of two expressions represents all possible combinations of words from the first expression followed by words from the second expression.
The input is a string expression with length between 1 and 60, which ensures that we do not need to handle extremely large strings, but the nesting of braces and concatenation can make the set of generated words grow exponentially. The output is a sorted list of all unique words generated from the expression.
Key edge cases include nested braces like "{{a,b},{b,c}}", repeated elements, or deeply concatenated expressions like "a{b,c}{d,e}f{g,h}". A naive approach could easily mismanage nested scopes or duplicate words, so careful parsing and set operations are necessary.
Approaches
The brute-force approach would try to evaluate the expression recursively by generating all combinations, but without proper parsing, it would fail to handle nested braces and unions correctly. This could involve recursively expanding all subexpressions and taking the union of results wherever commas appear. While this is conceptually correct, it would be inefficient and error-prone for deeply nested or concatenated expressions.
The optimal approach involves using a stack-based parser with two types of stacks: one for operands (sets of words) and one for operators (to manage concatenation and union). We process the expression character by character, handling letters, commas, and braces according to precedence rules. The key insight is that we can treat concatenation implicitly whenever two expressions are adjacent, and we only explicitly handle union with commas or end of braces. By using sets at every stage, duplicates are naturally removed.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^n) | O(2^n) | Recursively expand every subexpression, inefficient for nesting and concatenation |
| Optimal | O(3^n) | O(3^n) | Stack-based parsing with set operations handles unions and concatenations efficiently |
Algorithm Walkthrough
-
Initialize two stacks:
opsfor operators andexprsfor operand sets. -
Define a helper function to perform a union of sets and a concatenation of sets.
-
Iterate over the expression character by character:
-
If the character is a lowercase letter, push it as a singleton set onto
exprs. -
If the character is
{, push it ontoopsto indicate the start of a new subexpression scope. -
If the character is
}, repeatedly pop operators fromopsand apply them to sets fromexprsuntil encountering{. -
If the character is
,, treat it as a union operator. Process any previous concatenation if needed before pushing the union. -
Implicitly handle concatenation by checking if two sets are adjacent without an operator between them. Multiply the sets to form all possible concatenations.
-
At the end of the iteration, apply any remaining operations to collapse all sets into a final result set.
-
Convert the final set to a sorted list and return.
Why it works: The algorithm maintains the invariant that exprs always contains valid sets for the current scope, and ops controls whether these sets should be unioned or concatenated. By carefully processing braces and commas, the algorithm correctly interprets nested expressions and adjacency-based concatenation.
Python Solution
from typing import List
class Solution:
def braceExpansionII(self, expression: str) -> List[str]:
def multiply(set1, set2):
return {a + b for a in set1 for b in set2}
exprs = []
ops = []
i = 0
while i < len(expression):
char = expression[i]
if char.isalpha():
exprs.append({char})
elif char == '{':
ops.append(char)
elif char == '}':
while ops and ops[-1] != '{':
op = ops.pop()
set2 = exprs.pop()
set1 = exprs.pop()
exprs.append(set1.union(set2))
ops.pop() # pop the '{'
elif char == ',':
while ops and ops[-1] == ',':
op = ops.pop()
set2 = exprs.pop()
set1 = exprs.pop()
exprs.append(set1.union(set2))
ops.append(char)
# Handle implicit concatenation
if i + 1 < len(expression):
next_char = expression[i + 1]
if (char.isalpha() or char == '}') and (next_char.isalpha() or next_char == '{'):
set2 = exprs.pop()
set1 = exprs.pop()
exprs.append(multiply(set1, set2))
i += 1
while ops:
op = ops.pop()
set2 = exprs.pop()
set1 = exprs.pop()
exprs.append(set1.union(set2))
return sorted(exprs[-1])
The Python implementation uses a stack-based parsing approach. Each letter is pushed as a singleton set. Braces manage scope, and commas indicate union operations. Implicit concatenation is handled by checking adjacency of expressions. The multiply helper ensures that concatenation produces all possible combinations, and the final result is sorted.
Go Solution
package main
import (
"sort"
)
func braceExpansionII(expression string) []string {
multiply := func(a, b map[string]struct{}) map[string]struct{} {
res := make(map[string]struct{})
for x := range a {
for y := range b {
res[x+y] = struct{}{}
}
}
return res
}
exprs := []map[string]struct{}{}
ops := []rune{}
addSet := func(s map[string]struct{}) {
exprs = append(exprs, s)
}
i := 0
for i < len(expression) {
char := expression[i]
if char >= 'a' && char <= 'z' {
addSet(map[string]struct{}{string(char): {}})
} else if char == '{' {
ops = append(ops, '{')
} else if char == '}' {
for len(ops) > 0 && ops[len(ops)-1] != '{' {
op := ops[len(ops)-1]
ops = ops[:len(ops)-1]
set2 := exprs[len(exprs)-1]
exprs = exprs[:len(exprs)-1]
set1 := exprs[len(exprs)-1]
exprs = exprs[:len(exprs)-1]
for k := range set2 {
set1[k] = struct{}{}
}
exprs = append(exprs, set1)
}
ops = ops[:len(ops)-1] // pop '{'
} else if char == ',' {
for len(ops) > 0 && ops[len(ops)-1] == ',' {
op := ops[len(ops)-1]
ops = ops[:len(ops)-1]
set2 := exprs[len(exprs)-1]
exprs = exprs[:len(exprs)-1]
set1 := exprs[len(exprs)-1]
exprs = exprs[:len(exprs)-1]
for k := range set2 {
set1[k] = struct{}{}
}
exprs = append(exprs, set1)
}
ops = append(ops, ',')
}
// Handle implicit concatenation
if i+1 < len(expression) {
next_char := expression[i+1]
if (char >= 'a' && char <= 'z' || char == '}') && (next_char >= 'a' && next_char <= 'z' || next_char == '{') {
set2 := exprs[len(exprs)-1]
exprs = exprs[:len(exprs)-1]
set1 := exprs[len(exprs)-1]
exprs = exprs[:len(exprs)-1]
exprs = append(exprs, multiply(set1, set2))
}
}
i++
}
for len(ops) > 0 {
op := ops[len(ops)-1]
ops = ops[:len(ops)-1]
set2 := exprs[len(exprs)-1]
exprs = exprs[:len(exprs)-1]
set1 := exprs[len(exprs)-1]
exprs = exprs[:len(exprs)-1]
for k := range set2 {
set1[k] = struct{}{}
}
exprs = append(exprs, set1)
}
res := make([]string, 0, len(exprs[0]))
for k := range exprs[0] {
res = append(res, k)
}
sort.Strings(res)
return res
}
The Go solution mirrors the Python stack-based approach. The main difference is the use of map[string]struct{} to represent sets, as Go does not have a built-in set