318. Maximum Product of Word Lengths

318. Maximum Product of Word Lengths

Problem Solving - Day 4

ยท

8 min read

Hello, reader ๐Ÿ‘‹๐Ÿฝ ! Welcome to day 4 of the series on Problem Solving. Through this series, I aim to pick up at least one question everyday and share my approach for solving it.

Today, I'll be solving a random question picked up from LeetCode called Maximum Product of Word Lengths. Link to the question.


๐Ÿค” Problem Statement

Given an array of words, we have to return the maximum product of length of words word[i] and word[j] such that the two words have no characters in common.

  • E.g.: words = ["abcw", "baz", "foo", "bar", "xtfn", "abcdef"] => "abcw" and "xtfn" have no letters in common, hence the max product is 16.
  • Another e.g.: words = ["a","aa","aaa","aaaa"] => all the words have at least 1 character in common. Hence max product is 0.

The characters in the array only contain lowercase English alphabets


๐Ÿ’ฌ Thought Process (Frequency List)

  • Intuition says we need to process each word in the array and note down the list of character in it using a data structure like a set or a frequency array of length 26.
  • So that's exactly what I did. Traversed through every word in the array and noted down in a list of frequency array that had the count for each character.
  • Then once we know the frequencies of all the characters, we can then compare characters in words i and j such that, 0 <= i < j <= n.
  • If any one character in word[i] and word[j] are found to be common, we ignore this pair. Else, we compute the product of length of these 2 words and then compare it with the maximum product.

๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ’ป Solution 1

  • Below is the code for the above approach:
class Solution {
    public int maxProduct(String[] words) {
        int maxLen = 0;
        int n = words.length;
        List<int[]> frequencyList = new ArrayList<>();

        for(String word: words) {
            int[] frequency = new int[26];
            for(int i = 0; i<word.length(); i++) {
                char ch = word.charAt(i);
                frequency[ch-'a']++;
            }
            frequencyList.add(frequency);
        }

        for(int i = 0; i<n-1; i++) {
            String first = words[i];
            int firstLen = first.length();

            for(int j = i+1; j<n; j++) {
                String second = words[j];
                int secondLen = second.length();

                if(compareFrequency(first, second, frequencyList.get(i), frequencyList.get(j))) {
                    int len = firstLen * secondLen;
                    maxLen = Math.max(maxLen, len);
                }


            }
        }
        return maxLen;
    }

    private boolean compareFrequency(String first, String second, int[] firstFreq, int[] secondFreq) {
        for(int i = 0; i<26; i++) {
            int secondCount = secondFreq[i];
            int firstCount = firstFreq[i];

            // If both have the character, then the count will be greater than 0
            if(firstCount > 0 && secondCount > 0) {
                return false;
            }
        }
        return true;
    }
}
Time Complexity: O(n * (n + m))
    n = length of array words, m = max length of string
Space Complexity: O(n) 
    n = length of array words

๐Ÿ’ฌ Thought Process - Solution 2 (Bitset)

  • In the above approach, we are keeping the count of every character. We actually don't require this.

  • All we need to know is whether a character is present in a word or not. So, we can use something like a set or even a boolean contains array of length 26.

  • We create an array of length 26 for each word and mark character[j] at word[i]as true, if character j exists in word, else false.

๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ’ป Solution 2

  • Below is the code using the boolean array that marks true or false to specify the existence of a character.
class Solution {
    public int maxProduct(String[] words) {
        int maxLen = 0;
        int n = words.length;
        boolean[][] frequency = new boolean[n][26];

        for(int i = 0; i<n; i++) {
            String word = words[i];

            for(int j = 0; j<word.length(); j++) {
                char ch = word.charAt(j);
                frequency[i][ch-'a'] = true;
            }
        }

        for(int i = 0; i<n-1; i++) {
            String firstWord = words[i];

            for(int j = i+1; j<n; j++) {
                String secondWord = words[j];
                int secondLen = secondWord.length();

                if(compareFrequency(firstWord, secondWord, frequency[i], frequency[j])) {
                    int firstWordLen = firstWord.length();
                    int secondWordLen = secondWord.length();

                    int len = firstWordLen * secondWordLen;
                    maxLen = Math.max(maxLen, len);
                }
            }
        }
        return maxLen;
    }

    private boolean compareFrequency(String first, String second, boolean[] fWordFreq, boolean[] sWordFreq) {
        for(int i = 0; i<26; i++) {
            boolean firstFreq = fWordFreq[i];
            boolean secondFreq = sWordFreq[i];

            // If firstFreq and seconFreq are true, i.e. both contain the character i,
            // then they have a common character. We return false.
            if(firstFreq && secondFreq) {
                return false;
            }
        }

        // no common character found
        return true;
    }
}
Time Complexity: O(n * (n + m))
    n = length of array words, m = max length of string
Space Complexity: O(n) 
    n = length of array words
  • The time complexity and space used still remains same, but the use of a 2D array instead of a List in java could somewhat (although not much) improve the speed.

๐Ÿ’ฌ Thought Process - Solution 3 (Bit Masking)

  • Honestly, this was not something I thought of. After I submitted the 2 approaches, I checked out the discussion forum (and the question tag). I was surprised to see everyone using bit masking!
  • The idea is to generate a bit mask of size 26, and mark the bit at any index as 1 if character is present. We will do this in 3 steps:
    1. Get the character position in the 26 bit mask
    2. Set the bit at the character position if it's a part of the string
    3. Get the mask for the entire word
  • An example for a bit mask for word abc => 111
    • Where 0th bit represents a, 1st bit represents b, and 3rd bit represents c. The bits are set for all 3 positions as they appear in the string.
  • Another example is: ade => 11001
  • If we encounter a, then we make the 0th bit as 1. When we encounter b, then we make the 1st bit as 1. So on, until 25th bit for z, which we turn into 1 if the word contains a z.

Step 1: Get position of character in bit mask

  • To get the position of a character in the 26 bit mask, we will subtract a from every character. This means:
      'a' - 'a' => 0
      'b' - 'a' => 1
      'c' - 'a' => 2
      ...
      'z' - 'a' => 25
    
  • We are able to do this because characters in are represented as Unicode (in Java) or ASCII values.
  • So the positions of a, b, and c, appearing in the word abc are: 0, 1, and 2 respectively.

  • So far good. We converted our characters to represent their position in a 26 bit mask. But how do we set a bit if it occurs in the word?

Step 2: Set bit if character appears in word

  • We left shift 1 i times for every character. Here, i represents the position of the character in the bit.
  • E.g. for the characters in abc, we left shift 1:
    • 0 times for a => 1 << 0 => 1
    • 1 times for b => 1 << 1 => 10
    • 2 times for c => 2 << 1 => 100
  • So the mask at a => 0, b => 10, and c => 100

Step 3: Get mask for the entire word

  • To get the mask for the entire word, we need to somehow combine the individual character masks.
  • We combine by using | (Bitwise OR operator).
  • mask("abc") => maska || maskb || maskc => 1 || 10 || 100 => 111

And the bit mask is ready.

  • To check if any 2 words have same characters, all we do is perform & (Bitwise AND) operation.
  • If the & of the bit masks generates:
    • 0 => No characters are in common between 2 words.
    • 1 => Some characters are common between 2 words.

With this, let's check out the solution.

๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ’ป Solution 3

  • Below is the code that uses bit masking to resolve characters that occur commonly in twp strings:
class MaxProductWordLengthsBitMask {
    public int maxProduct(String[] words) {
        int n = words.length;
        int maxLen = 0;

        int[] bitmasks = getWordMasks(words, n);

        // Compares every word i with every other word j. [0 <= i < j < n]
        for(int i = 0; i<n-1; i++) {
            String firstWord = words[i];
            for(int j = 0; j<n; j++) {
                String secondWord = words[j];

                if(compareCharacters(firstWord, secondWord, bitmasks[i], bitmasks[j])) {
                    int firstWordLen = firstWord.length();
                    int secondWordLen = secondWord.length();
                    int len = firstWordLen * secondWordLen;
                    maxLen = Math.max(len, maxLen);
                }
            }
        }

        return maxLen;
    }

    private int[] getWordMasks(String[] words, int n) {
        int[] bitmasks = new int[n];

        for(int i = 0; i<n; i++) {
            String word = words[i];
            bitmasks[i] = generateWordMask(word);
        }

        return bitmasks;
    }

    private boolean compareCharacters(String firstWord, String secondWord, int firstBitMask, int secondBitMask) {
        return (firstBitMask & secondBitMask) == 0;
    }

    private int generateWordMask(String word) {
        int value = 0;
        int wordLen = word.length();

        for(int j = 0; j<wordLen; j++) {
            char ch = word.charAt(j);
            int position = getCharacterPositionInBit(ch);
            int mask = getCharacterMaskWithSetBit(position);

            // This creates a mask such that 
            // if ch = 'a' => 1
            // if ch = 'b' => 10
            // if ch = 'c' => 100
            // ...
            // if ch == 'z' => 1000...00 (25 0s)
            value |= mask;
        }

        return value;
    }

    private int getCharacterPositionInBit(char ch) {
        return ch - 'a';
    }

    private int getCharacterMaskWithSetBit(int position) {
        return 1 << position;
    }
}
Time Complexity: O(n * n) 
    n = length of words array
Space Complexity: O(n)
    n = length of words array
  • This approach would be very optimized since bit operations are faster.


These are the 3 approaches that I could think of and find. If you know of any other approach that is optimised, do share it in the comments below!

Find the code for all the approaches here.

Conclusion

That's a wrap for today's problem. If you liked my explanation then please do drop a like/ comment. Also, please correct me if I've made any mistakes or if you want me to improve something!

Thank you for reading!