Taro Logo

Maximum Score Words Formed by Letters

Hard
Google logo
Google
3 views
Topics:
ArraysStringsDynamic ProgrammingBit Manipulation

You are given a list of words, a list of single characters letters (which might contain duplicates), and an array score representing the point value of each letter of the alphabet (a-z). The goal is to find the maximum possible score that can be achieved by forming a valid set of words using the given letters. Each word can be used at most once, and each letter can be used at most as many times as it appears in the letters list.

For example, if words = ["dog","cat","dad","good"], letters = ["a","a","c","d","d","d","g","o","o"], and score = [1,0,9,5,0,0,3,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0] (where score[0] is the score for 'a', score[1] is the score for 'b', and so on), the maximum score is 23. This is achieved by using the words "dad" (score: 5 + 1 + 5 = 11) and "good" (score: 3 + 2 + 2 + 5 = 12). The total score is 11 + 12 = 23.

Another example: words = ["xxxz","ax","bx","cx"], letters = ["z","a","b","c","x","x","x"], score = [4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,10]. In this case, the maximum score is 27 by forming the words "ax", "bx", and "cx".

What is the most efficient algorithm to solve this problem? Discuss its time and space complexity. What are some edge cases to consider?

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. Can a word in the `words` array be used more than once, or is each word limited to a single use?
  2. Are the letters in the `letters` array case-sensitive? Should I assume all letters are lowercase?
  3. If it's not possible to form any words from the available letters, what should the function return: 0, or should it throw an error?
  4. How large can the input arrays `words` and `letters` be? Are there any constraints on the length of individual words?
  5. If multiple combinations of words yield the same maximum score, is any one of them acceptable, or is there a tie-breaking rule?

Brute Force Solution

Approach

We need to find the highest possible score by picking words from a list, but we only have a limited number of each letter. The brute force method tries every single combination of words to see which one gives the best score while still respecting the letter limits.

Here's how the algorithm would work step-by-step:

  1. Start by considering an empty selection of words, giving a score of zero.
  2. Then, try using only the first word, and check if you have enough letters to form it. If you do, calculate its score.
  3. Next, try using only the second word, then only the third word, and so on for all single words.
  4. Now, consider all pairs of words, again checking if you have enough letters to form both words. If you do, calculate the score for each valid pair.
  5. Continue doing this for all possible combinations of three words, four words, and so on, until you've considered using all the words together.
  6. For each combination of words, calculate the total score, but only if you have enough of each letter to form all the words in that combination.
  7. Keep track of the highest score you found amongst all valid combinations.
  8. The highest score found is your final answer.

Code Implementation

def max_score_words_brute_force(words, letters, score):
    max_score_found = 0

    number_of_words = len(words)

    for i in range(1 << number_of_words):
        word_combination = []
        for j in range(number_of_words):
            if (i >> j) & 1:
                word_combination.append(words[j])

        #Check if the current combination is possible given letter counts
        letter_counts = {}
        for letter in letters:
            letter_counts[letter] = letter_counts.get(letter, 0) + 1

        possible = True
        for word in word_combination:
            for letter in word:
                if letter in letter_counts and letter_counts[letter] > 0:
                    letter_counts[letter] -= 1
                else:
                    possible = False
                    break
            if not possible:
                break

        if possible:
            #Calculate score for the current combination
            current_score = 0
            for word in word_combination:
                for letter in word:
                    current_score += score[ord(letter) - ord('a')]

            # Store the highest score found
            max_score_found = max(max_score_found, current_score)

    return max_score_found

Big(O) Analysis

Time Complexity
O(2^n)The brute force approach explores all possible subsets of the input words. Given n words, there are 2^n possible subsets to consider. For each subset, we need to check if the letters are sufficient and calculate the score, which takes O(m) where m is the total number of letters across all words. Therefore, the dominant factor is the enumeration of all subsets, leading to a time complexity of O(2^n * m). Assuming m grows slower than 2^n, the overall time complexity is O(2^n).
Space Complexity
O(1)The described brute force method, while exploring all combinations, primarily uses a counter to track letter availability. This counter, representing letter counts, will have a fixed size (26 for the English alphabet) regardless of the number of words (N). The temporary score variable to store the highest score also takes up constant space. Therefore, the auxiliary space used by the algorithm is constant, independent of the input size N.

Optimal Solution

Approach

The key is to use a method that efficiently checks if we can form each word using the available letters and then maximize the score. We will use a technique called backtracking, where we explore possibilities and undo them if they don't lead to a good solution. This avoids checking every single combination of words.

Here's how the algorithm would work step-by-step:

  1. First, count the available letters you have to work with.
  2. Then, for each word, check if you have enough letters to form it. If not, skip it.
  3. If you can form a word, provisionally 'use' the letters. Calculate the score you would get by using that word.
  4. Now, try to add more words using the remaining letters, repeating the process of checking letter availability and calculating scores.
  5. Keep track of the highest score you've seen so far. If you run out of letters or words, that is one potential solution.
  6. After trying to add a word, 'undo' your choice by returning the letters to your inventory, so that you can explore other possibilities. This 'undoing' step is the backtracking part.
  7. Repeat steps 2-6 for all the different combinations of words until all the possibilities have been explored. The highest score you kept track of is your answer.

Code Implementation

def maximum_score_words_formed_by_letters(words, letters, score):
    letter_counts = {}
    for letter in letters:
        letter_counts[letter] = letter_counts.get(letter, 0) + 1

    max_score = 0

    def calculate_score(word):
        word_score = 0
        for char in word:
            word_score += score[ord(char) - ord('a')]
        return word_score

    def backtrack(index, current_letter_counts, current_score):
        nonlocal max_score
        max_score = max(max_score, current_score)

        #Iterate through all words to find the best possible combination
        for i in range(index, len(words)):
            word = words[i]
            word_letter_counts = {}
            can_form = True

            #Checking if we have enough letters to form each word
            for char in word:
                word_letter_counts[char] = word_letter_counts.get(char, 0) + 1
                if current_letter_counts.get(char, 0) < word_letter_counts[char]:
                    can_form = False
                    break

            if can_form:

                #Temporarily 'use' the letters from the available count
                for char in word:
                    current_letter_counts[char] -= 1

                #Explore the next word combination
                backtrack(i + 1, current_letter_counts, current_score + calculate_score(word))

                #Backtrack, restoring the original counts
                for char in word:
                    current_letter_counts[char] += 1

    backtrack(0, letter_counts.copy(), 0)

    return max_score

Big(O) Analysis

Time Complexity
O(2^n)The algorithm uses backtracking to explore all possible combinations of words. In the worst-case scenario, we might consider including or excluding each of the 'n' words in the input 'words' array. This results in a binary decision (include/exclude) for each word. Therefore, the time complexity grows exponentially with the number of words. Specifically, the algorithm effectively explores 2^n possible subsets of the input words.
Space Complexity
O(N)The algorithm's space complexity is dominated by the recursion depth of the backtracking function. In the worst-case scenario, where each word is considered and added to the solution, the depth of the recursion could reach N, where N is the number of words in the input list 'words'. Each level of the recursion stores the current state (letter counts, current score), leading to a space usage proportional to the depth of the recursion. Therefore, the auxiliary space complexity is O(N).

Edge Cases

CaseHow to Handle
Empty words arrayReturn 0 immediately, as no score can be achieved with no words.
Empty letters arrayReturn 0 immediately, as no word can be formed without available letters.
Empty score arrayTreat as all scores are 0; calculate the score normally, it will be 0.
Word requires more letters than available in the letters arrayThe algorithm should not use words that cannot be formed from the available letters, preventing the formation of invalid solutions.
All words are impossible to form given lettersThe algorithm should correctly return 0 as the maximum score when no valid words can be constructed.
Maximum sized input (large number of words, large words, large letters array)Consider using memoization or dynamic programming to avoid exponential time complexity from brute force recursion.
Scores array contains zero or negative valuesThe algorithm should correctly handle zero or negative scores by incorporating them into the score calculation, potentially reducing the overall score.
The letters array contains special characters or numbers when the words array only contains alphabetic charactersThe algorithm should ignore the invalid characters by validating the letters array input and proceeding without them.