Facebook Pixel

318. Maximum Product of Word Lengths

MediumBit ManipulationArrayString
Leetcode Link

Problem Description

You are given an array of strings words. Your task is to find two words from this array that don't share any common letters, and maximize the product of their lengths.

For each pair of words that have no letters in common, calculate the product of their lengths. Return the maximum such product. If no two words exist without common letters, return 0.

For example:

  • If words = ["abcw", "baz", "foo", "bar", "xtfn", "abcdef"]
  • The words "abcw" and "xtfn" share no common letters
  • Their lengths are 4 and 4, giving a product of 16
  • This would be one valid product to consider

The solution uses bit manipulation to efficiently check if two words share common letters. Each word is represented as a binary number where each bit position corresponds to a letter (a-z). If bit position i is set to 1, it means the word contains the letter at position i of the alphabet.

When two words don't share any common letters, the bitwise AND of their binary representations equals 0. The algorithm:

  1. Creates a binary mask for each word by setting appropriate bits for each letter present
  2. For each word, checks all previously processed words
  3. If two words have no common letters (their masks AND to 0), calculates the product of their lengths
  4. Keeps track of the maximum product found
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that checking if two words share common letters is fundamentally a set intersection problem. We need to determine if the set of letters in one word overlaps with the set of letters in another word.

Initially, we might think of using hash sets or iterating through each character of both words to check for common letters. However, this would be inefficient, especially when we need to compare every pair of words.

Since we're dealing with lowercase English letters (only 26 possible characters), we can use a clever optimization: represent each word's character set as a single integer using bit manipulation. Each of the 26 bits in an integer can represent whether a specific letter (a-z) is present in the word.

For example:

  • The word "abc" would have bits 0, 1, and 2 set (representing 'a', 'b', 'c')
  • The word "def" would have bits 3, 4, and 5 set (representing 'd', 'e', 'f')

Why is this powerful? Because checking if two words share common letters becomes a simple bitwise AND operation. If two words have no common letters, their bit representations will have no overlapping 1s, so mask1 & mask2 = 0.

This transforms our problem from a string comparison problem to a bit manipulation problem:

  1. Pre-compute a bitmask for each word (O(total characters in all words))
  2. Compare pairs of words using a single bitwise operation (O(1) per comparison)

The beauty of this approach is that we reduce the complexity of checking common letters from O(length1 × length2) to O(1) after the initial preprocessing, making the overall solution much more efficient.

Solution Approach

The implementation uses bit manipulation to efficiently track and compare character sets between words.

Step 1: Initialize Data Structures

  • Create a mask array of the same length as words to store the bitmask representation of each word
  • Initialize ans = 0 to track the maximum product

Step 2: Process Each Word For each word at index i:

  1. Build the bitmask: Iterate through each character c in the current word

    • Calculate the bit position: ord(c) - ord("a") gives us a value from 0-25
    • Set the corresponding bit: mask[i] |= 1 << (ord(c) - ord("a"))
    • The |= operator ensures we set the bit without affecting other already-set bits
  2. Compare with previous words: Check all words with index j < i

    • Perform bitwise AND: mask[i] & mask[j]
    • If the result equals 0, the words share no common letters
    • Update the answer: ans = max(ans, len(words[i]) * len(words[j]))

Example Walkthrough: Let's say we have words = ["abc", "def", "ab"]

  • For "abc": mask[0] = 0b111 (bits 0, 1, 2 set)
  • For "def": mask[1] = 0b111000 (bits 3, 4, 5 set)
    • Check with "abc": 0b111 & 0b111000 = 0 ✓ No common letters
    • Update ans = 3 * 3 = 9
  • For "ab": mask[2] = 0b11 (bits 0, 1 set)
    • Check with "abc": 0b11 & 0b111 = 0b11 ≠ 0 ✗ Has common letters
    • Check with "def": 0b11 & 0b111000 = 0 ✓ No common letters
    • Update ans = max(9, 2 * 3) = 9

Time Complexity: O(n² + N) where n is the number of words and N is the total number of characters across all words Space Complexity: O(n) for storing the bitmasks

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example with words = ["ac", "bed", "fg"] to illustrate how the bit manipulation solution works.

Step 1: Build bitmask for "ac" (index 0)

  • For 'a': position = 0, set bit 0: mask[0] = 0b1
  • For 'c': position = 2, set bit 2: mask[0] = 0b101
  • Final mask for "ac": 0b101 (bits 0 and 2 are set)

Step 2: Build bitmask for "bed" (index 1)

  • For 'b': position = 1, set bit 1: mask[1] = 0b10
  • For 'e': position = 4, set bit 4: mask[1] = 0b10010
  • For 'd': position = 3, set bit 3: mask[1] = 0b11010
  • Final mask for "bed": 0b11010 (bits 1, 3, and 4 are set)

Now check "bed" against all previous words:

  • Compare with "ac": 0b11010 & 0b101 = 0b0 ✓ (no common letters)
  • Product = length("bed") × length("ac") = 3 × 2 = 6
  • Update ans = 6

Step 3: Build bitmask for "fg" (index 2)

  • For 'f': position = 5, set bit 5: mask[2] = 0b100000
  • For 'g': position = 6, set bit 6: mask[2] = 0b1100000
  • Final mask for "fg": 0b1100000 (bits 5 and 6 are set)

Now check "fg" against all previous words:

  • Compare with "ac": 0b1100000 & 0b101 = 0b0 ✓ (no common letters)
    • Product = 2 × 2 = 4 (less than current ans = 6)
  • Compare with "bed": 0b1100000 & 0b11010 = 0b0 ✓ (no common letters)
    • Product = 2 × 3 = 6 (equal to current ans = 6)

Final Result: Maximum product = 6 (from "ac" and "bed")

The key insight: By representing each word as a bitmask, we can check for common letters with a single AND operation instead of comparing every character pair.

Solution Implementation

1class Solution:
2    def maxProduct(self, words: List[str]) -> int:
3        """
4        Find the maximum product of lengths of two words that don't share common letters.
5      
6        Args:
7            words: List of strings containing only lowercase letters
8          
9        Returns:
10            Maximum product of lengths of two words with no common letters
11        """
12        # Create bitmasks to represent which letters are present in each word
13        # Each bit position represents a letter (a=0, b=1, ..., z=25)
14        bitmasks = [0] * len(words)
15        max_product = 0
16      
17        # Process each word and create its bitmask
18        for i, word in enumerate(words):
19            # Set bits for each character in the current word
20            for char in word:
21                # Set the bit at position (char - 'a')
22                # For example: 'a' sets bit 0, 'b' sets bit 1, etc.
23                bitmasks[i] |= 1 << (ord(char) - ord('a'))
24          
25            # Check current word against all previously processed words
26            for j in range(i):
27                # If bitwise AND is 0, the words share no common letters
28                if (bitmasks[i] & bitmasks[j]) == 0:
29                    # Calculate product of lengths and update maximum
30                    current_product = len(word) * len(words[j])
31                    max_product = max(max_product, current_product)
32      
33        return max_product
34
1class Solution {
2    public int maxProduct(String[] words) {
3        int wordCount = words.length;
4        // Array to store bitmask representation of each word
5        // Each bit position represents a letter (a=0, b=1, ..., z=25)
6        int[] bitmasks = new int[wordCount];
7        int maxProduct = 0;
8      
9        // Process each word
10        for (int i = 0; i < wordCount; ++i) {
11            // Create bitmask for current word
12            // Set bit at position (character - 'a') to 1 if character exists
13            for (char character : words[i].toCharArray()) {
14                bitmasks[i] |= 1 << (character - 'a');
15            }
16          
17            // Compare current word with all previously processed words
18            for (int j = 0; j < i; ++j) {
19                // Check if two words share no common letters
20                // Bitwise AND of their masks should be 0 if no common letters
21                if ((bitmasks[i] & bitmasks[j]) == 0) {
22                    // Calculate product of lengths and update maximum
23                    int currentProduct = words[i].length() * words[j].length();
24                    maxProduct = Math.max(maxProduct, currentProduct);
25                }
26            }
27        }
28      
29        return maxProduct;
30    }
31}
32
1class Solution {
2public:
3    int maxProduct(vector<string>& words) {
4        int n = words.size();
5      
6        // Bitmask array to store character presence for each word
7        // Each bit position represents a letter (a-z)
8        vector<int> bitmasks(n, 0);
9      
10        int maxProduct = 0;
11      
12        // Process each word
13        for (int i = 0; i < n; ++i) {
14            // Build bitmask for current word
15            // Set bit at position (character - 'a') to 1 if character exists
16            for (char c : words[i]) {
17                bitmasks[i] |= (1 << (c - 'a'));
18            }
19          
20            // Compare current word with all previous words
21            for (int j = 0; j < i; ++j) {
22                // Check if words share no common characters
23                // If bitwise AND is 0, they have no common letters
24                if ((bitmasks[i] & bitmasks[j]) == 0) {
25                    // Update maximum product if current product is larger
26                    int currentProduct = static_cast<int>(words[i].size() * words[j].size());
27                    maxProduct = max(maxProduct, currentProduct);
28                }
29            }
30        }
31      
32        return maxProduct;
33    }
34};
35
1/**
2 * Finds the maximum product of lengths of two words that don't share common letters
3 * @param words - Array of words containing only lowercase English letters
4 * @returns Maximum product of lengths of two words with no common letters
5 */
6function maxProduct(words: string[]): number {
7    const wordCount: number = words.length;
8    // Bitmask array to store character presence for each word
9    // Each bit position represents a letter (a=0, b=1, ..., z=25)
10    const characterMasks: number[] = Array(wordCount).fill(0);
11    let maxProductValue: number = 0;
12  
13    // Process each word
14    for (let currentIndex = 0; currentIndex < wordCount; ++currentIndex) {
15        // Build bitmask for current word by setting bits for each character
16        for (const character of words[currentIndex]) {
17            // Set the bit corresponding to the character
18            // 'a' maps to bit 0, 'b' to bit 1, etc.
19            const bitPosition: number = character.charCodeAt(0) - 'a'.charCodeAt(0);
20            characterMasks[currentIndex] |= 1 << bitPosition;
21        }
22      
23        // Compare current word with all previous words
24        for (let previousIndex = 0; previousIndex < currentIndex; ++previousIndex) {
25            // Check if words share any common characters using bitwise AND
26            // If result is 0, no common characters exist
27            if ((characterMasks[currentIndex] & characterMasks[previousIndex]) === 0) {
28                // Calculate product of lengths and update maximum if needed
29                const currentProduct: number = words[currentIndex].length * words[previousIndex].length;
30                maxProductValue = Math.max(maxProductValue, currentProduct);
31            }
32        }
33    }
34  
35    return maxProductValue;
36}
37

Time and Space Complexity

Time Complexity: O(n^2 + L)

The time complexity consists of two main components:

  • Building the bitmask for each word: For each word i in the array, we iterate through all its characters. This takes O(L) time total, where L is the sum of lengths of all strings.
  • Comparing pairs of words: For each word i, we compare it with all previous words j < i using the bitmask AND operation. This results in O(n^2) comparisons, where n is the number of words.

Therefore, the overall time complexity is O(n^2 + L).

Space Complexity: O(n)

The space complexity is determined by:

  • The mask array which stores one integer bitmask for each of the n words, requiring O(n) space.
  • Other variables like ans, i, j, s, t, and c use constant space O(1).

Therefore, the overall space complexity is O(n).

Common Pitfalls

1. Duplicate Characters Within a Word

A common misconception is thinking that duplicate characters in a word need special handling or might affect the bitmask differently.

The Pitfall: Beginners might try to count character frequencies or use more complex data structures, thinking that "aaa" needs different treatment than "a".

Why It's Not a Problem: The bitwise OR operation (|=) is idempotent - setting a bit that's already set doesn't change anything. Whether a word contains 'a' once or multiple times, bit position 0 will simply be set to 1.

# Both "a" and "aaa" produce the same bitmask
word1 = "a"    # bitmask: 0b1
word2 = "aaa"  # bitmask: 0b1 (same!)

2. Incorrect Bit Position Calculation

Using the wrong formula to calculate which bit to set can lead to incorrect results or runtime errors.

The Pitfall:

# WRONG: Might cause negative bit positions or incorrect mapping
bitmasks[i] |= 1 << (ord('a') - ord(char))  # Reversed subtraction
bitmasks[i] |= 1 << ord(char)               # No normalization to 0-25

The Solution: Always use ord(char) - ord('a') to ensure:

  • 'a' maps to bit 0
  • 'z' maps to bit 25
  • All positions are within the valid range [0, 25]

3. Checking All Pairs Instead of Previous Words Only

Some might iterate through all pairs (i, j) where i ≠ j, leading to duplicate comparisons.

The Pitfall:

# INEFFICIENT: Checks each pair twice
for i in range(len(words)):
    for j in range(len(words)):
        if i != j and (bitmasks[i] & bitmasks[j]) == 0:
            max_product = max(max_product, len(words[i]) * len(words[j]))

The Solution: Only check j < i since multiplication is commutative. Checking "abc" vs "def" gives the same product as "def" vs "abc", so we only need to check once.

4. Integer Overflow Concerns (Language-Specific)

While Python handles arbitrary-precision integers automatically, in languages like Java or C++, you might worry about bitmask overflow with 26 bits.

Why It's Not a Problem: A 32-bit integer can handle all 26 letters (we only need 26 bits), and the product of two word lengths typically fits in standard integer types for reasonable input constraints.

5. Forgetting Edge Cases

Not handling arrays with fewer than 2 words or empty strings properly.

The Solution: The provided code naturally handles these:

  • If len(words) < 2: The inner loop never executes, returning 0
  • Empty strings: Create a bitmask of 0, work correctly with the logic
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More