1255. Maximum Score Words Formed By Letters - Explanation
Prerequisites
Before attempting this problem, you should be comfortable with:
- Backtracking - Used to explore all possible subsets of words by making include/exclude decisions at each step
- Hash Maps / Frequency Arrays - Needed to track letter counts and determine if a word can be formed with available letters
- Recursion - The core technique for exploring the decision tree of word combinations
- Bit Manipulation (optional) - Alternative approach using bitmasks to represent subsets
1. Backtracking
Intuition
We need to select a subset of words to maximize total score, where each word consumes letters from a shared pool. This is a classic subset selection problem. For each word, we decide whether to include it (if we have enough letters) or skip it. We explore all valid combinations using recursion and backtracking, restoring the letter counts after each recursive call.
Algorithm
- Build a frequency count of available letters.
- Define a recursive function that processes words one by one:
- Base case: if all words are processed, return
0. - Always try skipping the current word.
- If the current word can be formed with available letters:
- Subtract its letter requirements from the count.
- Recursively process remaining words and add the word's score.
- Restore the letter counts (backtrack).
- Base case: if all words are processed, return
- Return the maximum score from either skipping or including the word.
class Solution:
def maxScoreWords(self, words: List[str], letters: List[str], score: List[int]) -> int:
def can_form_word(w, letter_cnt):
word_cnt = Counter(w)
for c in word_cnt:
if word_cnt[c] > letter_cnt[c]:
return False
return True
def get_score(w):
res = 0
for c in w:
res += score[ord(c) - ord('a')]
return res
letter_cnt = Counter(letters)
def backtrack(i):
if i == len(words):
return 0
res = backtrack(i + 1) # skip
if can_form_word(words[i], letter_cnt): # include (when possible)
for c in words[i]:
letter_cnt[c] -= 1
res = max(res, get_score(words[i]) + backtrack(i + 1))
for c in words[i]:
letter_cnt[c] += 1
return res
return backtrack(0)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of words, is the maximum length of a word, is the size of the array , and is the size of the array .
2. Backtracking + Precomputation
Intuition
The basic backtracking approach recalculates letter frequencies and scores for each word during recursion. We can speed this up by precomputing the frequency array and score for each word upfront. This avoids redundant character-by-character processing and makes the validity check and score lookup constant time operations.
Algorithm
- Build a frequency count of available letters.
- Precompute for each word:
- Its character frequency array (
26elements for each letter). - Its total score based on the given scoring array.
- Its character frequency array (
- Use backtracking as before, but now:
- Check validity by comparing frequency arrays (
26comparisons). - Update letter counts by subtracting/adding the precomputed frequencies.
- Use the precomputed score directly.
- Check validity by comparing frequency arrays (
class Solution:
def maxScoreWords(self, words: List[str], letters: List[str], score: List[int]) -> int:
letter_cnt = [0] * 26
for c in letters:
letter_cnt[ord(c) - ord('a')] += 1
n = len(words)
word_scores = [0] * n
word_freqs = [[0] * 26 for _ in range(n)]
for i, word in enumerate(words):
for c in word:
idx = ord(c) - ord('a')
word_freqs[i][idx] += 1
word_scores[i] += score[idx]
def backtrack(i):
if i == n:
return 0
res = backtrack(i + 1) # skip
can_include = all(word_freqs[i][j] <= letter_cnt[j] for j in range(26))
if can_include: # include (when possible)
for j in range(26):
letter_cnt[j] -= word_freqs[i][j]
res = max(res, word_scores[i] + backtrack(i + 1))
for j in range(26):
letter_cnt[j] += word_freqs[i][j]
return res
return backtrack(0)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of words, is the maximum length of a word, is the size of the array , and is the size of the array .
3. Backtracking (Bit Mask)
Intuition
Instead of recursive backtracking, we can iterate through all possible subsets using bit manipulation. With n words, there are 2^n possible subsets, each representable as an integer where bit i indicates whether word i is included. For each subset (bitmask), we check if all included words can be formed simultaneously and calculate the total score.
Algorithm
- Precompute frequency arrays and scores for all words.
- Iterate through all bitmasks from
0to2^n - 1:- For each bitmask, create a copy of the available letter counts.
- For each bit set in the mask:
- Check if the corresponding word can be formed with remaining letters.
- If not, mark this subset as invalid and break.
- If yes, subtract the word's letters and add its score.
- If the subset is valid, update the maximum score.
- Return the maximum score found.
class Solution:
def maxScoreWords(self, words: List[str], letters: List[str], score: List[int]) -> int:
letter_cnt = [0] * 26
for c in letters:
letter_cnt[ord(c) - ord('a')] += 1
n = len(words)
word_scores = [0] * n
word_freqs = [[0] * 26 for _ in range(n)]
for i, word in enumerate(words):
for c in word:
idx = ord(c) - ord('a')
word_freqs[i][idx] += 1
word_scores[i] += score[idx]
res = 0
for mask in range(1 << n):
cur_score = 0
cur_letter_cnt = letter_cnt[:]
valid = True
for i in range(n):
if mask & (1 << i):
for j in range(26):
if word_freqs[i][j] > cur_letter_cnt[j]:
valid = False
break
if not valid:
break
for j in range(26):
cur_letter_cnt[j] -= word_freqs[i][j]
cur_score += word_scores[i]
if valid:
res = max(res, cur_score)
return resTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of words, is the maximum length of a word, is the size of the array , and is the size of the array .
Common Pitfalls
Not Restoring Letter Counts After Backtracking
When using backtracking, you must restore the letter counts after exploring the branch where you include a word. Forgetting to add the letters back after the recursive call means subsequent branches operate with incorrect letter availability, leading to wrong results or missing valid combinations.
Checking Word Validity Too Late
You should verify that a word can be formed with available letters before consuming those letters. If you subtract letters first and then discover the word is invalid, you've already corrupted the state. Always check availability first, then subtract if the word can be formed.
Counting Letters Incorrectly for Duplicate Characters
When a word contains duplicate characters (e.g., "aab"), you need the letter pool to have enough of each character. A common bug is checking if each character exists at least once, rather than checking if the count is sufficient. Use frequency arrays or counters that properly track multiple occurrences.
Confusing Word Score with Letter Score
The score array maps each letter to its value (index 0 = 'a', index 1 = 'b', etc.). A word's score is the sum of its letters' scores, not a single lookup. Misinterpreting this mapping or forgetting to sum all letters in the word produces incorrect scores.
Using Greedy Instead of Exhaustive Search
This problem requires exploring all 2^n subsets of words because including a high-scoring word might prevent including multiple other words that together score higher. Greedy approaches (e.g., always taking the highest-scoring valid word first) don't guarantee the optimal solution.