1255. Maximum Score Words Formed by Letters
Problem Description
You are given three inputs:
- A list of
words
- A list of single
letters
(which may contain duplicates) - A
score
array wherescore[i]
represents the score for the i-th letter of the alphabet ('a' has scorescore[0]
, 'b' has scorescore[1]
, and so on)
Your task is to form a valid set of words from the given word list that maximizes the total score. A valid set must follow these rules:
- Each word from the list can be used at most once (you cannot use the same word twice)
- The total letters used across all selected words cannot exceed the available letters in the
letters
list - Each letter in
letters
can only be used once
The score of a valid set is calculated by summing up the scores of all individual letters used in the selected words.
For example, if you have:
words = ["dog", "cat"]
letters = ["d", "o", "g", "c", "a", "t"]
score = [1, 2, 3, ...]
(where 'a'=1, 'b'=2, 'c'=3, 'd'=4, etc.)
You could select both "dog" and "cat" since you have enough letters. The score would be the sum of scores for 'd', 'o', 'g', 'c', 'a', 't'.
You need to return the maximum possible score that can be achieved by selecting any valid subset of words (including the empty set, which has a score of 0). You don't have to use all the available letters.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem involves selecting words from a list to maximize score, not traversing nodes and edges in a graph structure.
Need to solve for kth smallest/largest?
- No: We're looking for the maximum score, not the kth smallest or largest element.
Involves Linked Lists?
- No: The problem works with arrays/lists of words and letters, not linked list data structures.
Does the problem have small constraints?
- Yes: The problem typically has small constraints (usually the word list has at most 14-15 words), which makes it feasible to explore all possible combinations. With n words, we have 2^n possible subsets to consider.
Brute force / Backtracking?
- Yes: Given the small constraints and the need to explore all possible combinations of words to find the maximum score, brute force or backtracking is the appropriate approach. We need to try different combinations of words, check if each combination is valid (has enough letters), and track the maximum score.
Conclusion: The flowchart suggests using a backtracking/brute force approach. This aligns perfectly with the solution, which uses binary enumeration to explore all 2^n possible subsets of words, validates each subset against the available letters, and tracks the maximum score achieved.
Intuition
The key insight is recognizing that we need to find the best subset of words from our word list. Since each word can either be included or excluded from our selection, we're essentially dealing with a subset selection problem.
Why can we use brute force here? Look at the constraints - with a small number of words (typically 14-15), we have at most 2^15 = 32,768
possible combinations to check. This is computationally manageable.
The challenge breaks down into three parts:
- Generate all possible word combinations - We need a way to systematically explore every possible subset of words
- Validate each combination - Check if we have enough letters to form all the words in our current subset
- Calculate and track the maximum score - For valid combinations, compute the total score and keep track of the best one
Binary enumeration is a clever technique for generating all subsets. We can represent each subset as a binary number where each bit position corresponds to a word. For example, with 3 words ["dog", "cat", "dad"]
:
001
(binary) means select only "dad"101
(binary) means select "dog" and "dad"111
(binary) means select all three words
For validation, we count how many of each letter we need for our selected words and compare against what's available. If we need 2 'a's but only have 1 'a' available, that combination is invalid.
The scoring is straightforward - for each letter used in valid combinations, we add up their individual scores based on the given score array.
This approach guarantees we find the optimal solution because we literally check every possible way to select words, making it impossible to miss the best combination.
Learn more about Dynamic Programming, Backtracking and Bitmask patterns.
Solution Approach
The implementation uses binary enumeration to explore all possible word combinations systematically. Let's walk through the solution step by step:
Step 1: Count Available Letters
cnt = Counter(letters)
We first create a frequency map of available letters. This tells us exactly how many of each letter we can use. For example, if letters = ["a", "a", "b", "c"]
, then cnt = {'a': 2, 'b': 1, 'c': 1}
.
Step 2: Binary Enumeration Setup
for i in range(1 << n):
We iterate through all numbers from 0
to 2^n - 1
, where n
is the number of words. The expression 1 << n
is equivalent to 2^n
. Each number i
represents a unique subset of words through its binary representation.
Step 3: Extract Selected Words
cur = Counter(''.join([words[j] for j in range(n) if i >> j & 1]))
For each subset i
, we extract the selected words. The condition i >> j & 1
checks if the j
-th bit of i
is set:
i >> j
shiftsi
right byj
positions& 1
checks if the least significant bit is 1
If the bit is set, we include words[j]
. We then join all selected words into a single string and count the frequency of each letter needed.
Step 4: Validate the Combination
if all(v <= cnt[c] for c, v in cur.items()):
We check if we have enough letters for the current word combination. For each letter c
needed with count v
, we verify that v <= cnt[c]
(we need at most as many as we have available).
Step 5: Calculate Score
t = sum(v * score[ord(c) - ord('a')] for c, v in cur.items())
ans = max(ans, t)
If the combination is valid, we calculate its total score. For each letter c
used v
times:
ord(c) - ord('a')
converts the letter to its index (0 for 'a', 1 for 'b', etc.)score[ord(c) - ord('a')]
gets the score for that letter- We multiply by
v
to account for multiple uses of the same letter
We keep track of the maximum score seen across all valid combinations.
Time Complexity: O(2^n × m)
where n
is the number of words and m
is the average length of words. We check 2^n
subsets, and for each subset, we need O(m)
time to count letters and calculate scores.
Space Complexity: O(1)
extra space (not counting the input), as we only use counters with at most 26 entries for English letters.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to illustrate the solution approach:
Given:
words = ["ace", "bed", "face"]
letters = ["a", "c", "e", "f", "b"]
score = [1, 9, 1, 2, 4, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
- This means: 'a'=1, 'b'=9, 'c'=1, 'd'=2, 'e'=4, 'f'=2, etc.
Step 1: Count Available Letters
cnt = {'a': 1, 'c': 1, 'e': 1, 'f': 1, 'b': 1}
Step 2: Binary Enumeration (n=3 words, so we check 2^3 = 8 combinations)
Let's trace through each subset:
Binary | Decimal | Selected Words | Letters Needed | Valid? | Score Calculation |
---|---|---|---|---|---|
000 | 0 | none | {} | ✓ | 0 |
001 | 1 | ["face"] | {f:1, a:1, c:1, e:1} | ✓ | 2+1+1+4 = 8 |
010 | 2 | ["bed"] | {b:1, e:1, d:1} | ✗ | - (no 'd' available) |
011 | 3 | ["bed", "face"] | {b:1, e:2, d:1, f:1, a:1, c:1} | ✗ | - (need 2 'e's, have 1) |
100 | 4 | ["ace"] | {a:1, c:1, e:1} | ✓ | 1+1+4 = 6 |
101 | 5 | ["ace", "face"] | {a:2, c:2, e:2, f:1} | ✗ | - (need 2 of each a,c,e) |
110 | 6 | ["ace", "bed"] | {a:1, c:1, e:2, b:1, d:1} | ✗ | - (need 2 'e's, no 'd') |
111 | 7 | all three | {a:2, c:2, e:3, b:1, d:1, f:1} | ✗ | - (multiple violations) |
Step 3: Detailed Look at Valid Combinations
For subset 001 (selecting "face"):
- Letters needed: f(1), a(1), c(1), e(1)
- All needed letters are available in our letter pool
- Score = score['f'] + score['a'] + score['c'] + score['e']
- Score = 2 + 1 + 1 + 4 = 8
For subset 100 (selecting "ace"):
- Letters needed: a(1), c(1), e(1)
- All needed letters are available
- Score = score['a'] + score['c'] + score['e']
- Score = 1 + 1 + 4 = 6
Step 4: Find Maximum The maximum score among all valid combinations is 8 (from selecting just "face").
Answer: 8
This example demonstrates how the algorithm systematically checks every possible word combination, validates whether we have sufficient letters, and tracks the maximum achievable score.
Solution Implementation
1class Solution:
2 def maxScoreWords(
3 self, words: List[str], letters: List[str], score: List[int]
4 ) -> int:
5 # Count available letters
6 available_letters = Counter(letters)
7 num_words = len(words)
8 max_score = 0
9
10 # Iterate through all possible subsets of words using bit manipulation
11 # Total subsets = 2^n where n is the number of words
12 for subset_mask in range(1 << num_words):
13 # Build the current subset of words
14 # Check each bit position to determine if word at that index is included
15 selected_words = []
16 for word_idx in range(num_words):
17 if (subset_mask >> word_idx) & 1:
18 selected_words.append(words[word_idx])
19
20 # Count letters needed for the current subset of words
21 required_letters = Counter(''.join(selected_words))
22
23 # Check if we have enough letters to form this subset
24 is_valid = True
25 for letter, count in required_letters.items():
26 if count > available_letters[letter]:
27 is_valid = False
28 break
29
30 # If valid subset, calculate its score
31 if is_valid:
32 current_score = 0
33 for letter, count in required_letters.items():
34 # Get score for each letter (a=0, b=1, ..., z=25)
35 letter_score = score[ord(letter) - ord('a')]
36 current_score += count * letter_score
37
38 # Update maximum score
39 max_score = max(max_score, current_score)
40
41 return max_score
42
1class Solution {
2 public int maxScoreWords(String[] words, char[] letters, int[] score) {
3 // Count frequency of each available letter
4 int[] availableLetters = new int[26];
5 for (int i = 0; i < letters.length; i++) {
6 availableLetters[letters[i] - 'a']++;
7 }
8
9 int wordCount = words.length;
10 int maxScore = 0;
11
12 // Iterate through all possible subsets of words using bit manipulation
13 // Total subsets = 2^n where n is number of words
14 for (int subset = 0; subset < (1 << wordCount); subset++) {
15 // Track letter usage for current subset
16 int[] usedLetters = new int[26];
17
18 // Check which words are included in current subset
19 for (int wordIndex = 0; wordIndex < wordCount; wordIndex++) {
20 // Check if wordIndex-th bit is set in subset
21 if (((subset >> wordIndex) & 1) == 1) {
22 // Count letters from this word
23 String currentWord = words[wordIndex];
24 for (int charIndex = 0; charIndex < currentWord.length(); charIndex++) {
25 usedLetters[currentWord.charAt(charIndex) - 'a']++;
26 }
27 }
28 }
29
30 // Validate if we have enough letters and calculate score
31 boolean isValidSubset = true;
32 int currentScore = 0;
33
34 for (int letterIndex = 0; letterIndex < 26; letterIndex++) {
35 // Check if we're using more letters than available
36 if (usedLetters[letterIndex] > availableLetters[letterIndex]) {
37 isValidSubset = false;
38 break;
39 }
40 // Calculate score for used letters
41 currentScore += usedLetters[letterIndex] * score[letterIndex];
42 }
43
44 // Update maximum score if current subset is valid and has higher score
45 if (isValidSubset && currentScore > maxScore) {
46 maxScore = currentScore;
47 }
48 }
49
50 return maxScore;
51 }
52}
53
1class Solution {
2public:
3 int maxScoreWords(vector<string>& words, vector<char>& letters, vector<int>& score) {
4 // Count frequency of each available letter
5 int availableLetters[26] = {0};
6 for (char& letter : letters) {
7 availableLetters[letter - 'a']++;
8 }
9
10 int numWords = words.size();
11 int maxScore = 0;
12
13 // Iterate through all possible subsets of words using bit masking
14 // (1 << numWords) generates 2^numWords combinations
15 for (int mask = 0; mask < (1 << numWords); ++mask) {
16 // Count letters needed for current subset of words
17 int lettersNeeded[26] = {0};
18
19 // Check which words are included in current subset
20 for (int wordIdx = 0; wordIdx < numWords; ++wordIdx) {
21 // If bit at position wordIdx is set, include this word
22 if ((mask >> wordIdx) & 1) {
23 // Count letters in the selected word
24 for (char& letter : words[wordIdx]) {
25 lettersNeeded[letter - 'a']++;
26 }
27 }
28 }
29
30 // Validate if we have enough letters and calculate score
31 bool isValidSubset = true;
32 int currentScore = 0;
33
34 for (int letterIdx = 0; letterIdx < 26; ++letterIdx) {
35 // Check if we have enough of this letter
36 if (lettersNeeded[letterIdx] > availableLetters[letterIdx]) {
37 isValidSubset = false;
38 break;
39 }
40 // Add score for all occurrences of this letter
41 currentScore += lettersNeeded[letterIdx] * score[letterIdx];
42 }
43
44 // Update maximum score if current subset is valid and has higher score
45 if (isValidSubset && currentScore > maxScore) {
46 maxScore = currentScore;
47 }
48 }
49
50 return maxScore;
51 }
52};
53
1function maxScoreWords(words: string[], letters: string[], score: number[]): number {
2 // Count frequency of each available letter
3 const availableLetters: number[] = new Array(26).fill(0);
4 for (const letter of letters) {
5 availableLetters[letter.charCodeAt(0) - 'a'.charCodeAt(0)]++;
6 }
7
8 const numWords = words.length;
9 let maxScore = 0;
10
11 // Iterate through all possible subsets of words using bit masking
12 // (1 << numWords) generates 2^numWords combinations
13 for (let mask = 0; mask < (1 << numWords); mask++) {
14 // Count letters needed for current subset of words
15 const lettersNeeded: number[] = new Array(26).fill(0);
16
17 // Check which words are included in current subset
18 for (let wordIdx = 0; wordIdx < numWords; wordIdx++) {
19 // If bit at position wordIdx is set, include this word
20 if ((mask >> wordIdx) & 1) {
21 // Count letters in the selected word
22 for (const letter of words[wordIdx]) {
23 lettersNeeded[letter.charCodeAt(0) - 'a'.charCodeAt(0)]++;
24 }
25 }
26 }
27
28 // Validate if we have enough letters and calculate score
29 let isValidSubset = true;
30 let currentScore = 0;
31
32 for (let letterIdx = 0; letterIdx < 26; letterIdx++) {
33 // Check if we have enough of this letter
34 if (lettersNeeded[letterIdx] > availableLetters[letterIdx]) {
35 isValidSubset = false;
36 break;
37 }
38 // Add score for all occurrences of this letter
39 currentScore += lettersNeeded[letterIdx] * score[letterIdx];
40 }
41
42 // Update maximum score if current subset is valid and has higher score
43 if (isValidSubset && currentScore > maxScore) {
44 maxScore = currentScore;
45 }
46 }
47
48 return maxScore;
49}
50
Time and Space Complexity
Time Complexity: O(2^n × n × M)
The algorithm iterates through all possible subsets of words using bit manipulation (1 << n
generates 2^n
iterations). For each subset:
- It constructs a string by joining selected words, which takes
O(n × M)
time in the worst case where alln
words are selected and each word has maximum lengthM
- Creating the Counter from the joined string takes
O(n × M)
time - Checking if all characters in
cur
are available incnt
takesO(C)
time whereC = 26
(alphabet size) - Computing the score takes
O(C)
time
Since C
is constant (26), the dominant factor is O(2^n × n × M)
.
Space Complexity: O(C)
The space complexity is determined by:
cnt
: Counter for available letters, which stores at mostC = 26
distinct characterscur
: Counter for current subset's characters, which also stores at mostC = 26
distinct characters- The joined string created temporarily has size
O(n × M)
, but this is not persistent storage - Other variables (
ans
,t
, loop variables) useO(1)
space
The dominant persistent space usage is O(C)
where C = 26
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Incorrectly Handling Letter Reuse Across Words
The Problem: A common mistake is checking letter availability for each word independently rather than considering the cumulative letter usage across all selected words. For example:
# INCORRECT approach
for subset_mask in range(1 << num_words):
is_valid = True
for word_idx in range(num_words):
if (subset_mask >> word_idx) & 1:
# Checking each word against original letters independently
if not can_form_word(words[word_idx], available_letters):
is_valid = False
break
This fails because if you select words ["ace", "bad"], and you have letters ["a", "b", "c", "d", "e"], the letter "a" would be incorrectly counted as available for both words.
The Solution: Count all required letters across ALL selected words first, then check against available letters:
# CORRECT approach
selected_words = []
for word_idx in range(num_words):
if (subset_mask >> word_idx) & 1:
selected_words.append(words[word_idx])
# Count total letters needed for ALL selected words
required_letters = Counter(''.join(selected_words))
# Now check if we have enough letters in total
is_valid = all(count <= available_letters[letter]
for letter, count in required_letters.items())
Pitfall 2: Forgetting to Handle Missing Letters in Available Pool
The Problem: When checking if required letters are available, forgetting that some letters might not exist in the available pool at all:
# INCORRECT - This will raise KeyError if letter not in available_letters for letter, count in required_letters.items(): if count > available_letters[letter]: # KeyError if letter not present is_valid = False
The Solution:
Use the Counter's default behavior or use .get()
with a default value:
# CORRECT - Counter returns 0 for missing keys by default for letter, count in required_letters.items(): if count > available_letters[letter]: # Returns 0 if letter not present is_valid = False # Alternative using .get() for letter, count in required_letters.items(): if count > available_letters.get(letter, 0): is_valid = False
Pitfall 3: Off-by-One Error in Letter Score Indexing
The Problem: Incorrectly calculating the index for the score array:
# INCORRECT - Common mistakes
letter_score = score[ord(letter)] # Wrong: uses ASCII value directly
letter_score = score[ord(letter) - ord('a') + 1] # Wrong: off by one
The Solution: Remember that 'a' should map to index 0, 'b' to index 1, etc.:
# CORRECT
letter_score = score[ord(letter) - ord('a')]
# 'a': ord('a') - ord('a') = 0
# 'b': ord('b') - ord('a') = 1
# 'z': ord('z') - ord('a') = 25
What's the relationship between a tree and a graph?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Bitmask and Dynamic Programming Bit manipulation is a crucial aspect of computer programming and one of the most powerful tools for bit manipulation is bitmasks Let's first understand what a bit is A bit is a binary digit It's the smallest piece of data in a computer and can be
Want a Structured Path to Master System Design Too? Don’t Miss This!