318. Maximum Product of Word Lengths
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:
- Creates a binary mask for each word by setting appropriate bits for each letter present
- For each word, checks all previously processed words
- If two words have no common letters (their masks AND to 0), calculates the product of their lengths
- Keeps track of the maximum product found
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:
- Pre-compute a bitmask for each word (O(total characters in all words))
- 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 aswords
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
:
-
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
- Calculate the bit position:
-
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]))
- Perform bitwise AND:
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
- Check with "abc":
- 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
- Check with "abc":
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 EvaluatorExample 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 takesO(L)
time total, whereL
is the sum of lengths of all strings. - Comparing pairs of words: For each word
i
, we compare it with all previous wordsj < i
using the bitmask AND operation. This results inO(n^2)
comparisons, wheren
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 then
words, requiringO(n)
space. - Other variables like
ans
,i
,j
,s
,t
, andc
use constant spaceO(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
Which algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!