Problem
Given a list of strings, pick a subsequence of them (zero or more, in any order, each used at most once) and concatenate them together. Return the maximum length of such a concatenation where every character in the final string is unique.
- Input:
arr = ["un", "iq", "ue"] - Output:
4 - Explanation:
"un" + "iq" = "uniq"has four distinct characters. Adding"ue"would repeat'u', so the best we can do is length 4.
A string like "aab" can never appear in a valid result because it contains a duplicate 'a' internally โ it would make any concatenation invalid regardless of what else is included.
Intuition
Each string contributes a set of characters, and two strings can coexist in the concatenation only when their character sets are completely disjoint. Representing each character set as a 26-bit integer (one bit per letter) turns the overlap check into a single bitwise AND โ if the AND is zero, there is no shared character. Backtracking over all subsets then finds the longest valid combination without ever building or comparing strings character by character.
Solution โ Bitmask Backtracking
Precompute a bitmask for each string (skipping those with internal duplicates), then explore all valid subsequences with backtracking, pruning any branch where the candidate string overlaps the characters already chosen.
- For each string, walk its characters and set a bit for each letter in a 26-bit integer; if a bit is already set, the string has an internal duplicate and is discarded
- Collect the
(bitmask, length)pairs for all strings that passed the filter - Run backtracking from index 0 with an empty mask and length 0; update the global maximum at every node
- At each step, loop over remaining strings (from the current index forward so each string is used at most once)
- If a candidate's bitmask shares no bits with the current mask, include it and recurse with the combined mask and accumulated length
- Return the maximum length recorded
1def maxLength(arr: list[str]) -> int:
2 def get_bitmask(word):
3 mask = 0
4 for ch in word:
5 bit = 1 << (ord(ch) - ord('a'))
6 if mask & bit: # Duplicate character within this single word
7 return -1
8 mask |= bit
9 return mask
10
11 # Only keep words whose characters are all distinct
12 valid = [(get_bitmask(w), len(w)) for w in arr if get_bitmask(w) != -1]
13
14 max_length = 0
15
16 def backtrack(index, current_mask, current_length):
17 nonlocal max_length
18 max_length = max(max_length, current_length)
19 for i in range(index, len(valid)):
20 word_mask, word_len = valid[i]
21 if current_mask & word_mask == 0: # No character overlap with chosen set
22 backtrack(i + 1, current_mask | word_mask, current_length + word_len)
23
24 backtrack(0, 0, 0)
25 return max_lengthTime: O(2โฟ) โ each string is either included or excluded, giving at most 2โฟ subsets to explore; bitmask operations are O(1)
Space: O(n) โ the recursion stack depth is at most the number of valid strings
Complexity Summary
| Approach | Time | Space | When to use |
|---|---|---|---|
| Bitmask Backtracking | O(2โฟ) | O(n) | Always โ the constraint (strings contain only lowercase letters, length โค 26) keeps n small enough for exponential search |
Common Mistakes
- Not filtering words with internal duplicate characters โ a word like
"aab"will always break uniqueness regardless of what it's combined with; checking only against the global mask won't catch this, because the word is only ever compared to previously chosen strings - Starting the inner loop at index 0 instead of the current index โ this allows the same word to be picked more than once, which is invalid since we need a subsequence where each element appears at most once
- Using
current_mask & word_mask != 0as the condition to include โ the condition to add a word is that there is NO overlap, meaning the bitwise AND must equal zero, not be nonzero; this inversion produces the opposite behavior - Calling
get_bitmasktwice per word โ computing the mask once in the filter step and once in the valid list (as shown in the Python example above) doubles work; compute it once and store the result - Omitting the recursion base case and only updating max at leaf nodes โ since any prefix of a valid subsequence is also valid, the maximum can be achieved at an intermediate node, not just at depth n; update the max at every recursive call
Related Problems
subsetsโ core subset enumeration pattern that this solution builds oncombination-sum-iiโ backtracking with an index-forward loop to avoid reusing elementspalindrome-partitioningโ backtracking that prunes invalid branches before recursing deepermaximum-product-of-word-lengthsโ uses the exact same bitmask trick to check whether two words share any characters in O(1)permutationsโ backtracking over selections with a visited marker, contrasted with the index-forward approach used here