LeetCode 3435 - Frequencies of Shortest Supersequences
The problem asks us to find all shortest common supersequences (SCS) of a given list of two-character strings words. A shortest common supersequence is a string of minimum length that contains each word in words as a subsequence.
Difficulty: 🔴 Hard
Topics: Array, String, Bit Manipulation, Graph Theory, Topological Sort, Enumeration
Solution
Problem Understanding
The problem asks us to find all shortest common supersequences (SCS) of a given list of two-character strings words. A shortest common supersequence is a string of minimum length that contains each word in words as a subsequence. Importantly, the output should not include SCSs that are permutations of each other; instead, it should return the letter frequency arrays for each unique SCS. Each frequency array is of size 26, corresponding to the lowercase English letters.
The input consists of up to 256 strings, each of length 2. The total set of characters across all strings is limited to 16 unique letters, which strongly suggests that a bitmask or combinatorial approach can be feasible because the effective search space is small. The output is a 2D list, where each inner list contains the counts of letters in one SCS.
Edge cases include:
- Words that share no characters, where the SCS is simply the concatenation of all words.
- Words that share characters, where overlapping reduces the length of the SCS.
- Situations where multiple SCSs have the same letter frequencies due to permutations, which should be deduplicated.
The constraints imply that although the number of words can be large, the total number of unique characters is small, allowing for graph-based or bitmask dynamic programming solutions. Each input string has length exactly two. Therefore every word imposes one of two possible constraints:
- If the word is
"ab"witha != b, then any valid supersequence must contain an occurrence ofabefore an occurrence ofb. - If the word is
"aa", then any valid supersequence must contain at least two occurrences ofa.
The problem asks for all shortest common supersequences, but not as strings. Instead, we only need the frequency vector of each distinct multiset of letters. Two SCSs that are permutations of each other have identical frequency vectors, so only one frequency vector should be returned.
A crucial constraint is that all words together contain at most 16 distinct letters. Although there may be up to 256 words, the effective state space is determined by at most 16 vertices, which strongly suggests a bitmask solution.
The important observation is that every letter needs either one occurrence or two occurrences. No letter ever needs three or more occurrences because every constraint involves only a pair of letters.
The main edge cases are:
- Cycles such as
"ab"and"ba", which force at least one duplicated letter. - Larger directed cycles such as
"ab","bc","ca". - Self-loops such as
"aa", which force that letter to appear twice. - Independent components, where several different minimum solutions may exist.
Approaches
Brute Force
A naive approach would attempt to generate all possible orderings of the words and merge them into supersequences, checking each merged string for subsequence inclusion. After generating all supersequences, we would filter by minimum length and convert to frequency arrays while deduplicating permutations. This is correct but extremely inefficient because the number of permutations grows factorially with the number of words, and each merge operation requires scanning strings.
Optimal Approach
The key insight is that since each word is of length 2 and there are at most 16 unique letters, we can model the problem as a partial order graph. Each letter can be a node, and each word implies a directed edge from the first letter to the second. The problem then reduces to enumerating all topological sorts of this graph. Each topological sort gives a valid SCS with letters arranged respecting the order constraints. After computing all topological sorts, we convert each sort to a frequency array and deduplicate them based on letter counts.
This approach avoids generating all permutations of words directly and leverages the small alphabet size to keep the computational space manageable.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O((2n)!) | O(2n) | Generate all word merges and filter minimum-length supersequences. Too slow for n up to 256. |
| Optimal | O(2^k * k!) | O(2^k * k) | Build DAG from letter constraints, enumerate all topological sorts. k = number of unique letters (≤16). |
Algorithm Walkthrough
- Collect Unique Letters: Iterate over all words and collect all unique letters. Assign each letter an index for easier bitmask representation.
- Build DAG: For each word, add a directed edge from the first character to the second. This encodes precedence constraints.
- Compute In-Degrees: Track the number of incoming edges for each node. Nodes with zero in-degree can appear next in a topological sort.
- Enumerate Topological Sorts: Recursively generate all topological orders of the graph using a backtracking approach. At each step, pick a node with zero in-degree, append it to the current order, decrease in-degrees of its neighbors, and recurse. After recursion, backtrack by restoring in-degrees and removing the node from the current order.
- Convert to Frequencies: Each valid topological sort corresponds to an SCS. Count the occurrences of each letter to generate the 26-length frequency array.
- Deduplicate Frequencies: Since permutations of letters with the same counts are considered the same, store frequency arrays in a set or hashable structure to remove duplicates.
- Return Results: Convert unique frequency sets into a list and return.
Why it works: The topological sort ensures that all letter order constraints implied by the words are respected, guaranteeing that each word is a subsequence of any supersequence formed from a sort. Enumerating all valid topological sorts ensures we find all distinct SCSs. Deduplication by frequency ensures permutations are not counted multiple times. A direct approach would attempt to generate shortest common supersequences themselves.
One could view every word as a subsequence constraint and search over candidate strings, checking whether all constraints are satisfied. Even after restricting attention to shortest lengths, the number of possible strings grows exponentially with the number of letters and duplicated characters.
Since there can be up to 16 distinct letters, this approach is completely infeasible.
Key Insight
Construct a directed graph on the distinct letters.
For every word "ab" with a != b, add a directed edge a -> b.
For every word "aa", mark a as a letter that must appear twice.
Suppose we decide that a set D of letters will appear twice. Every other letter appears once.
A fundamental fact is:
A frequency assignment is feasible if and only if every directed cycle contains at least one duplicated letter.
Equivalently, after removing all duplicated letters, the remaining graph must be acyclic.
Therefore the problem becomes:
- Find all minimum-cardinality sets
Dthat contain every mandatory duplicated letter coming from self-loops. - After removing
D, the remaining graph is a DAG.
This is exactly a minimum feedback vertex set problem on at most 16 vertices, which is small enough for complete subset enumeration.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | Exponential in supersequence length | Exponential | Enumerates candidate supersequences |
| Optimal | O(2^m · m²) | O(m) | Enumerates duplicated-letter sets, where m ≤ 16 |
Algorithm Walkthrough
Step 1: Collect the Relevant Letters
Extract all distinct letters appearing in the input.
Since the total number of distinct letters is at most 16, map them to indices 0..m-1.
Step 2: Build the Constraint Graph
For every word:
- If the two characters are different, add a directed edge.
- If the two characters are equal, mark that letter as mandatory duplicated.
Let mandatoryMask denote all letters forced to appear twice.
Step 3: Enumerate Candidate Duplication Sets
Every valid duplication set must contain mandatoryMask.
Let allMask = (1 << m) - 1.
Enumerate every subset D satisfying
mandatoryMask ⊆ D
The letters in D will have frequency 2.
All other letters will have frequency 1.
Step 4: Check Whether the Remaining Graph Is Acyclic
Let
R = allMask \ D
be the letters that appear only once.
The duplication set is feasible exactly when the graph induced by R is acyclic.
Perform a topological-removal test:
- Repeatedly remove vertices whose incoming edges from
Rare zero. - If all vertices are removed, the graph is acyclic.
- Otherwise a cycle remains.
Step 5: Keep Only Minimum Solutions
Among all feasible duplication sets, compute the minimum size.
Only sets with this minimum size contribute shortest supersequences.
The resulting length is
m + |D|
because every duplicated letter contributes one extra occurrence.
Step 6: Build Frequency Vectors
For each minimum feasible duplication set:
- Frequency is 2 for letters in
D. - Frequency is 1 for all other used letters.
- Frequency is 0 for unused alphabet letters.
Store the resulting 26-element array.
Why it works
A duplicated letter can appear once before and once after any cycle involving that letter. Thus every cycle can be broken by duplicating at least one vertex on the cycle.
Conversely, if a cycle remains among letters that appear only once, those letters must occur in a cyclic ordering, which is impossible.
Therefore a duplication set is feasible exactly when its removal destroys all directed cycles. Minimizing the number of duplicated letters minimizes the supersequence length, and every minimum feasible duplication set yields one distinct frequency vector.
Python Solution
from typing import List
from collections import defaultdict
class Solution:
def supersequences(self, words: List[str]) -> List[List[int]]:
letters = sorted(set(c for word in words for c in word))
idx = {c: i for i, c in enumerate(letters)}
n = len(letters)
# Build graph
graph = defaultdict(list)
in_degree = [0] * n
for word in words:
u, v = idx[word[0]], idx[word[1]]
graph[u].append(v)
in_degree[v] += 1
results = set()
def backtrack(path, visited, in_deg):
if len(path) == n:
freq = [0] * 26
for i in path:
freq[ord(letters[i]) - ord('a')] += 1
results.add(tuple(freq))
return
for i in range(n):
if not visited[i] and in_deg[i] == 0:
visited[i] = True
for nei in graph[i]:
in_deg[nei] -= 1
backtrack(path + [i], visited, in_deg)
for nei in graph[i]:
in_deg[nei] += 1
visited[i] = False
visited = [False] * n
backtrack([], visited, in_degree[:])
return [list(freq) for freq in results]
The implementation first maps each unique letter to an index, constructs a graph representing order constraints, then uses backtracking to enumerate all topological sorts. Each sort is converted into a frequency array, and duplicates are eliminated via a set.
class Solution: def supersequences(self, words: List[str]) -> List[List[int]]: letters = sorted(set("".join(words))) m = len(letters)
idx = {ch: i for i, ch in enumerate(letters)}
pred = [0] * m
mandatory_mask = 0
for word in words:
a = idx[word[0]]
b = idx[word[1]]
if a == b:
mandatory_mask |= 1 << a
else:
pred[b] |= 1 << a
all_mask = (1 << m) - 1
def is_acyclic(remaining: int) -> bool:
cur = remaining
while cur:
removable = 0
temp = cur
while temp:
bit = temp & -temp
v = bit.bit_length() - 1
if (pred[v] & cur) == 0:
removable |= bit
temp ^= bit
if removable == 0:
return False
cur ^= removable
return True
best = m + 1
answers = []
free_mask = all_mask ^ mandatory_mask
sub = free_mask
while True:
duplicate_mask = mandatory_mask | sub
remaining = all_mask ^ duplicate_mask
if is_acyclic(remaining):
cnt = duplicate_mask.bit_count()
if cnt < best:
best = cnt
answers = [duplicate_mask]
elif cnt == best:
answers.append(duplicate_mask)
if sub == 0:
break
sub = (sub - 1) & free_mask
result = []
for mask in answers:
freq = [0] * 26
for ch in letters:
freq[ord(ch) - ord('a')] = 1
temp = mask
while temp:
bit = temp & -temp
v = bit.bit_length() - 1
freq[ord(letters[v]) - ord('a')] = 2
temp ^= bit
result.append(freq)
return result
The implementation first compresses the used letters into indices `0..m-1`. The array `pred` stores incoming-edge bitmasks. A self-loop does not become a graph edge, instead it contributes to `mandatory_mask`.
The helper function `is_acyclic` performs a topological-removal process on a bitmask of remaining vertices. If at some point no vertex has zero in-degree within the current subgraph, a cycle exists.
All supersets of `mandatory_mask` are enumerated. For each candidate duplication set, the induced graph on nonduplicated letters is tested for acyclicity. The minimum feasible duplication count is recorded, and all minimum solutions are converted into frequency vectors.
## Go Solution
```go
package main
func supersequences(words []string) [][]int {
lettersMap := make(map[byte]bool)
for _, w := range words {
lettersMap[w[0]] = true
lettersMap[w[1]] = true
}
letters := make([]byte, 0, len(lettersMap))
idx := make(map[byte]int)
i := 0
for c := range lettersMap {
letters = append(letters, c)
idx[c] = i
i++
}
n := len(letters)
graph := make([][]int, n)
inDeg := make([]int, n)
for _, w := range words {
u, v := idx[w[0]], idx[w[1]]
graph[u] = append(graph[u], v)
inDeg[v]++
}
resultsMap := make(map[[26]int]bool)
var backtrack func(path []int, visited []bool, inDeg []int)
backtrack = func(path []int, visited []bool, inDeg []int) {
if len(path) == n {
var freq [26]int
for _, i := range path {
freq[letters[i]-'a']++
}
resultsMap[freq] = true
return
}
for i := 0; i < n; i++ {
if !visited[i] && inDeg[i] == 0 {
visited[i] = true
for _, nei := range graph[i] {
inDeg[nei]--
}
backtrack(append(path, i), visited, inDeg)
for _, nei := range graph[i] {
inDeg[nei]++
}
visited[i] = false
}
}
}
visited := make([]bool, n)
inDegCopy := make([]int, n)
copy(inDegCopy, inDeg)
backtrack([]int{}, visited, inDegCopy)
result := make([][]int, 0, len(resultsMap))
for freq := range resultsMap {
temp := make([]int, 26)
for i := 0; i < 26; i++ {
temp[i] = freq[i]
}
result = append(result, temp)
}
return result
}
The Go solution mirrors the Python approach but carefully handles slices and fixed-size arrays for frequency storage. Go maps with array keys are used to deduplicate results efficiently.
Worked Examples
Example 1: words = ["ab","ba"]
- Unique letters:
['a','b'] - Graph edges:
a -> bandb -> a - No valid linear order satisfies both, so topological sorts allow each letter to appear first, resulting in
"aba"and"bab". - Frequency arrays:
[1,2,...]and[2,1,...].
Example 2: words = ["aa","ac"]
- Unique letters:
['a','c'] - Graph edges:
a -> a(ignored for SCS since repeated letters allowed) anda -> c - Only one valid frequency array:
[2,0,1,...].
Example 3: words = ["aa","bb","cc"]
- Unique letters:
['a','b','c'] - No order constraints beyond single letters
import ( "math/bits" "sort" )
func supersequences(words []string) [][]int { letterSet := map[byte]bool{} for _, w := range words { letterSet[w[0]] = true letterSet[w[1]] = true }
letters := make([]byte, 0, len(letterSet))
for ch := range letterSet {
letters = append(letters, ch)
}
sort.Slice(letters, func(i, j int) bool {
return letters[i] < letters[j]
})
m := len(letters)
idx := make(map[byte]int)
for i, ch := range letters {
idx[ch] = i
}
pred := make([]int, m)
mandatoryMask := 0
for _, w := range words {
a := idx[w[0]]
b := idx[w[1]]
if a == b {
mandatoryMask |= 1 << a
} else {
pred[b] |= 1 << a
}
}
allMask := (1 << m) - 1
isAcyclic := func(remaining int) bool {
cur := remaining
for cur != 0 {
removable := 0
for v := 0; v < m; v++ {
if (cur&(1<<v)) == 0 {
continue
}
if (pred[v] & cur) == 0 {
removable |= 1 << v
}
}
if removable == 0 {
return false
}
cur ^= removable
}
return true
}
best := m + 1
var masks []int
freeMask := allMask ^ mandatoryMask
for sub := freeMask; ; sub = (sub - 1) & freeMask {
duplicateMask := mandatoryMask | sub
remaining := allMask ^ duplicateMask
if isAcyclic(remaining) {
cnt := bits.OnesCount(uint(duplicateMask))
if cnt < best {
best = cnt
masks = []int{duplicateMask}
} else if cnt == best {
masks = append(masks, duplicateMask)
}
}
if sub == 0 {
break
}
}
ans := make([][]int, 0, len(masks))
for _, mask := range masks {
freq := make([]int, 26)
for _, ch := range letters {
freq[int(ch-'a')] = 1
}
for v := 0; v < m; v++ {
if (mask & (1 << v)) != 0 {
freq[int(letters[v]-'a')] = 2
}
}
ans = append(ans, freq)
}
return ans
}
The Go version follows the same logic as the Python implementation. The only notable differences are the use of `bits.OnesCount` for population counts and explicit integer bitmask manipulation instead of Python's built-in `bit_count()`.
## Worked Examples
### Example 1
Input:
["ab", "ba"]
Graph:
a -> b b -> a
| Candidate D | Remaining Vertices | Acyclic? |
| --- | --- | --- |
| {} | {a,b} | No |
| {a} | {b} | Yes |
| {b} | {a} | Yes |
| {a,b} | {} | Yes |
Minimum duplication count is 1.
Frequency vectors:
a twice, b once -> [2,1,...] a once, b twice -> [1,2,...]
### Example 2
Input:
["aa", "ac"]
Mandatory duplication:
D must contain a
Graph:
a -> c
| Candidate D | Remaining | Acyclic? |
| --- | --- | --- |
| {a} | {c} | Yes |
| {a,c} | {} | Yes |
Minimum duplication count is 1.
Frequency vector:
a = 2 c = 1
Result:
[2,0,1,...]
### Example 3
Input:
["aa", "bb", "cc"]
No ordering edges.
Mandatory duplication set:
{a,b,c}
Remaining graph is empty.
Only frequency vector:
a = 2 b = 2 c = 2
## Complexity Analysis
| Measure | Complexity | Explanation |
| --- | --- | --- |
| Time | O(2^m · m²) | Enumerate all duplication subsets and perform DAG testing |
| Space | O(m) | Graph and bitmask storage |
Here `m` is the number of distinct letters, and the problem guarantees `m ≤ 16`. Therefore:
2^16 = 65536
which is easily manageable.
## Test Cases
```sql
s = Solution()
# Example 1
ans = s.supersequences(["ab", "ba"])
expected = {
tuple([2,1] + [0]*24),
tuple([1,2] + [0]*24),
}
assert {tuple(x) for x in ans} == expected # 2-cycle
# Example 2
assert s.supersequences(["aa", "ac"]) == [
[2,0,1] + [0]*23
] # mandatory duplication
# Example 3
assert s.supersequences(["aa", "bb", "cc"]) == [
[2,2,2] + [0]*23
] # only self-loops
# Single edge
assert s.supersequences(["ab"]) == [
[1,1] + [0]*24
] # simple DAG
# 3-cycle
ans = s.supersequences(["ab", "bc", "ca"])
expected = {
tuple([2,1,1] + [0]*23),
tuple([1,2,1] + [0]*23),
tuple([1,1,2] + [0]*23),
}
assert {tuple(x) for x in ans} == expected
# DAG chain
assert s.supersequences(["ab", "bc"]) == [
[1,1,1] + [0]*23
] # no duplication required
# Mandatory duplication plus cycle
ans = s.supersequences(["aa", "ab", "ba"])
assert ans == [
[2,1] + [0]*24
] # a already breaks cycle
# Disconnected cycles
ans = s.supersequences(["ab", "ba", "cd", "dc"])
assert len(ans) == 4 # choose one vertex from each cycle
Test Summary
| Test | Why |
|---|---|
["ab","ba"] |
Smallest nontrivial cycle |
["aa","ac"] |
Mandatory duplicated letter |
["aa","bb","cc"] |
Only self-loops |
["ab"] |
Simple DAG |
["ab","bc","ca"] |
Directed 3-cycle |
["ab","bc"] |
No cycle present |
["aa","ab","ba"] |
Mandatory duplication already breaks cycle |
["ab","ba","cd","dc"] |
Multiple independent cycles |
Edge Cases
Self-Loops
A word such as "aa" does not impose an ordering relation. Instead, it requires two occurrences of a. A common mistake is to insert a graph self-loop and treat it as a cycle to break later. The implementation handles this correctly by placing a directly into mandatory_mask.
Cycles of Length Greater Than Two
A cycle such as:
ab
bc
ca
cannot be resolved by topological ordering. At least one vertex must be duplicated. The implementation detects this because the induced graph on nonduplicated vertices fails the acyclicity test.
Multiple Independent Cycles
Consider:
ab, ba, cd, dc
There are two separate cycles. Breaking only one is insufficient. The algorithm examines every duplication set and keeps only those whose removal makes the entire remaining graph acyclic. Thus it correctly requires one duplicated letter from each cycle.
Already Acyclic Graphs
If the graph contains no cycles and there are no self-loops, the empty duplication set is feasible. The algorithm naturally discovers this and returns a single frequency vector with frequency one for every used letter.