916. Word Subsets - Explanation

Problem Link



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Character Frequency Counting - Using arrays or hash maps to count occurrences of each character in a string
  • Hash Maps - For efficient frequency lookups and comparisons
  • Greedy Optimization - Combining multiple constraints into a single merged requirement to reduce comparisons

1. Brute Force

Intuition

A word from words1 is "universal" if every word in words2 is a subset of it. For a word to be a subset of another, every character must appear at least as many times in the target word. The straightforward approach is to check each word in words1 against all words in words2, comparing character frequencies to determine if all words2 words are subsets of words1.

Algorithm

  1. Iterate through each word w1 in words1.
  2. Count the frequency of each character in w1.
  3. For each word w2 in words2, count its character frequencies.
  4. Compare the counts: if any character in w2 appears more times than in w1, mark w1 as not universal and break.
  5. If w1 passes all subset checks, add it to the res list.
  6. Return the list of universal words.
class Solution:
    def wordSubsets(self, words1: List[str], words2: List[str]) -> List[str]:
        res = []
        for w1 in words1:
            count1 = Counter(w1)
            is_subset = True

            for w2 in words2:
                count2 = Counter(w2)
                for c in count2:
                    if count2[c] > count1[c]:
                        is_subset = False
                        break

                if not is_subset: break

            if is_subset:
                res.append(w1)

        return res

Time & Space Complexity

  • Time complexity: O(Nn+NMm)O(N * n + N * M * m)
  • Space complexity:
    • O(1)O(1) extra space, since we have at most 2626 different characters.
    • O(Nn)O(N * n) space for the output list.

Where NN is the size of the array words1words1, nn is the length of the longest word in words1words1, MM is the size of the array words2words2, and mm is the length of the longest word in words2words2.


2. Greedy + Hash Map

Intuition

Instead of checking every word in words2 for each candidate, we can precompute a single "maximum requirement" array. The key insight is that if a word is universal, it must satisfy all words in words2 simultaneously. This means for each character, we only need the maximum count required across all words in words2. By merging all requirements into one frequency map, we reduce the problem to a single comparison per candidate word.

Algorithm

  1. Build a combined frequency map for words2: for each character, store the maximum count needed across all words.
  2. Iterate through each word w in words1.
  3. Count the frequency of each character in w.
  4. Compare against the combined requirement: if any character count in w is less than required, skip this word.
  5. If w meets all requirements, add it to the res list.
  6. Return the list of universal words.
class Solution:
    def wordSubsets(self, words1: List[str], words2: List[str]) -> List[str]:
        count_2 = defaultdict(int)
        for w in words2:
            count_w = Counter(w)
            for c, cnt in count_w.items():
                count_2[c] = max(count_2[c], cnt)

        res = []
        for w in words1:
            count_w = Counter(w)
            flag = True
            for c, cnt in count_2.items():
                if count_w[c] < cnt:
                    flag = False
                    break
            if flag:
                res.append(w)

        return res

Time & Space Complexity

  • Time complexity: O(Nn+Mm)O(N * n + M * m)
  • Space complexity:
    • O(1)O(1) extra space, since we have at most 2626 different characters.
    • O(Nn)O(N * n) space for the output list.

Where NN is the size of the array words1words1, nn is the length of the longest word in words1words1, MM is the size of the array words2words2, and mm is the length of the longest word in words2words2.