1255. Maximum Score Words Formed By Letters - Explanation

Problem Link



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

  1. Build a frequency count of available letters.
  2. 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).
  3. 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: O(2n(w+m)+N)O(2 ^ n * (w + m) + N)
  • Space complexity: O(n+w)O(n + w)

Where nn is the number of words, ww is the maximum length of a word, mm is the size of the array scoresscores, and NN is the size of the array lettersletters.


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

  1. Build a frequency count of available letters.
  2. Precompute for each word:
    • Its character frequency array (26 elements for each letter).
    • Its total score based on the given scoring array.
  3. Use backtracking as before, but now:
    • Check validity by comparing frequency arrays (26 comparisons).
    • Update letter counts by subtracting/adding the precomputed frequencies.
    • Use the precomputed score directly.
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: O(m2n+N)O(m * 2 ^ n + N)
  • Space complexity: O(n+w)O(n + w)

Where nn is the number of words, ww is the maximum length of a word, mm is the size of the array scoresscores, and NN is the size of the array lettersletters.


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

  1. Precompute frequency arrays and scores for all words.
  2. Iterate through all bitmasks from 0 to 2^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.
  3. 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 res

Time & Space Complexity

  • Time complexity: O(mn2n+N)O(m * n * 2 ^ n + N)
  • Space complexity: O(n+w)O(n + w)

Where nn is the number of words, ww is the maximum length of a word, mm is the size of the array scoresscores, and NN is the size of the array lettersletters.


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.

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.