Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

916. Word Subsets - Leetcode Solution

Code Implementation

from collections import Counter
from typing import List

class Solution:
    def wordSubsets(self, words1: List[str], words2: List[str]) -> List[str]:
        max_count = Counter()
        for b in words2:
            b_count = Counter(b)
            for char in b_count:
                max_count[char] = max(max_count[char], b_count[char])

        res = []
        for a in words1:
            a_count = Counter(a)
            if all(a_count[char] >= max_count[char] for char in max_count):
                res.append(a)
        return res
      
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

class Solution {
public:
    vector<string> wordSubsets(vector<string>& words1, vector<string>& words2) {
        vector<int> maxCount(26, 0);
        for (const string& b : words2) {
            vector<int> bCount(26, 0);
            for (char c : b)
                bCount[c - 'a']++;
            for (int i = 0; i < 26; ++i)
                maxCount[i] = max(maxCount[i], bCount[i]);
        }
        vector<string> res;
        for (const string& a : words1) {
            vector<int> aCount(26, 0);
            for (char c : a)
                aCount[c - 'a']++;
            bool valid = true;
            for (int i = 0; i < 26; ++i) {
                if (aCount[i] < maxCount[i]) {
                    valid = false;
                    break;
                }
            }
            if (valid)
                res.push_back(a);
        }
        return res;
    }
};
      
import java.util.*;

class Solution {
    public List<String> wordSubsets(String[] words1, String[] words2) {
        int[] maxCount = new int[26];
        for (String b : words2) {
            int[] bCount = new int[26];
            for (char c : b.toCharArray()) {
                bCount[c - 'a']++;
            }
            for (int i = 0; i < 26; ++i) {
                maxCount[i] = Math.max(maxCount[i], bCount[i]);
            }
        }
        List<String> res = new ArrayList<>();
        for (String a : words1) {
            int[] aCount = new int[26];
            for (char c : a.toCharArray()) {
                aCount[c - 'a']++;
            }
            boolean valid = true;
            for (int i = 0; i < 26; ++i) {
                if (aCount[i] < maxCount[i]) {
                    valid = false;
                    break;
                }
            }
            if (valid) res.add(a);
        }
        return res;
    }
}
      
/**
 * @param {string[]} words1
 * @param {string[]} words2
 * @return {string[]}
 */
var wordSubsets = function(words1, words2) {
    let maxCount = new Array(26).fill(0);
    for (let b of words2) {
        let bCount = new Array(26).fill(0);
        for (let c of b) {
            bCount[c.charCodeAt(0) - 97]++;
        }
        for (let i = 0; i < 26; ++i) {
            maxCount[i] = Math.max(maxCount[i], bCount[i]);
        }
    }
    let res = [];
    for (let a of words1) {
        let aCount = new Array(26).fill(0);
        for (let c of a) {
            aCount[c.charCodeAt(0) - 97]++;
        }
        let valid = true;
        for (let i = 0; i < 26; ++i) {
            if (aCount[i] < maxCount[i]) {
                valid = false;
                break;
            }
        }
        if (valid) res.push(a);
    }
    return res;
};
      

Problem Description

You are given two string arrays: words1 and words2. A string a from words1 is considered a universal word if for every string b in words2, a contains all the letters in b (including duplicates). In other words, for each character in b, a must have at least as many occurrences of that character as b does.

Your task is to return all universal words from words1 as an array.

  • Each character is a lowercase English letter.
  • There may be multiple strings in words2, and a universal word must satisfy the requirement for all of them.
  • Each word from words1 can be used at most once in the answer.

Thought Process

At first glance, it may seem necessary to check each word in words1 against every word in words2, verifying for each if all required letters are present in sufficient quantity. This brute-force approach, however, may become inefficient as the input size grows.

The key insight is that the requirements from words2 can be combined: for each letter, what is the maximum number of times it is needed in any words2 word? If a word from words1 meets these maximum requirements for every letter, it will be universal for all words2.

By transforming words2 into a single "maximum letter frequency" requirement, we can check each words1 word just once against this combined profile, greatly reducing the number of checks.

Solution Approach

We will follow these steps to solve the problem efficiently:

  1. Build the maximum letter requirement:
    • For each word in words2, count the frequency of each letter.
    • For each letter, record the highest frequency seen across all words2 words.
    • This results in a single "maximum requirement" for each letter.
  2. Check each word in words1:
    • For every word in words1, count its letter frequencies.
    • Compare these counts to the maximum requirements from step 1.
    • If the word meets or exceeds the requirement for every letter, it is universal and added to the result.
  3. Return the list of universal words.

We use hash maps (or arrays for 26 letters) for fast frequency counting and comparison.

Example Walkthrough

Input:
words1 = ["amazon", "apple", "facebook", "google", "leetcode"]
words2 = ["e", "o"]

  1. Build max letter requirement from words2:
    • "e" → {'e': 1}
    • "o" → {'o': 1}
    • Take max for each letter: {'e': 1, 'o': 1}
  2. Check each word in words1:
    • "amazon": {'a':2, 'm':1, 'z':1, 'o':1, 'n':1} → Meets 'o':1 but lacks 'e':1 → Not universal
    • "apple": {'a':1, 'p':2, 'l':1, 'e':1} → Meets 'e':1 but lacks 'o':1 → Not universal
    • "facebook": {'f':1, 'a':1, 'c':1, 'e':2, 'b':1, 'o':2, 'k':1} → Has at least 'e':1 and 'o':1 → Universal
    • "google": {'g':2, 'o':2, 'l':1, 'e':1} → Has at least 'e':1 and 'o':1 → Universal
    • "leetcode": {'l':1, 'e':3, 't':1, 'c':1, 'o':1, 'd':1} → Has at least 'e':1 and 'o':1 → Universal
  3. Output: ["facebook", "google", "leetcode"]

Time and Space Complexity

  • Brute-force approach:
    • For each word in words1 (length N), check against each word in words2 (length M), and for each letter (up to 26). Time: O(N * M * L), where L is the average word length.
  • Optimized approach (as above):
    • Building the max requirement: O(M * L)
    • For each word in words1, counting letters and comparing: O(N * L)
    • Total time: O(M * L + N * L), which is much faster.
    • Space: O(26) for letter counts, negligible compared to input size.

Summary

The Word Subsets problem can be efficiently solved by reducing the requirements from words2 into a single "maximum letter frequency" profile, and then checking each word in words1 against this profile. This avoids redundant checking and leverages fast frequency counting, resulting in a clear, elegant, and efficient solution.