Sentence Similarity III

Patrick Leaton
Problem Description

A sentence is a list of words that are separated by a single space with no leading or trailing spaces. For example, "Hello World""HELLO""hello world hello world" are all sentences. Words consist of only uppercase and lowercase English letters.

Two sentences sentence1 and sentence2 are similar if it is possible to insert an arbitrary sentence (possibly empty) inside one of these sentences such that the two sentences become equal. For example, sentence1 = "Hello my name is Jane" and sentence2 = "Hello Jane" can be made equal by inserting "my name is" between "Hello" and "Jane" in sentence2.

Given two sentences sentence1 and sentence2, return true if sentence1 and sentence2 are similar. Otherwise, return false.

 

Example 1:

Input: sentence1 = "My name is Haley", sentence2 = "My Haley"
Output: true
Explanation: sentence2 can be turned to sentence1 by inserting "name is" between "My" and "Haley".

Example 2:

Input: sentence1 = "of", sentence2 = "A lot of words"
Output: false
Explanation: No single sentence can be inserted inside one of the sentences to make it equal to the other.

Example 3:

Input: sentence1 = "Eating right now", sentence2 = "Eating"
Output: true
Explanation: sentence2 can be turned to sentence1 by inserting "right now" at the end of the sentence.

Example 4:

Input: sentence1 = "Luky", sentence2 = "Lucccky"
Output: false

 

Constraints:

  • 1 <= sentence1.length, sentence2.length <= 100
  • sentence1 and sentence2 consist of lowercase and uppercase English letters and spaces.
  • The words in sentence1 and sentence2 are separated by a single space.

 

The description was taken from https://leetcode.com/problems/sentence-similarity-iii/.

Problem Solution

#Two Queues
#O(M+N) Time, O(M+N) Space
class Solution:
    def areSentencesSimilar(self, sentence1: str, sentence2: str) -> bool:
        queue_one = collections.deque(sentence1.split())
        queue_two = collections.deque(sentence2.split())
       
        while queue_one and queue_two:
            if queue_one[0] != queue_two[0]:
                break
            queue_one.popleft()
            queue_two.popleft()
 
        if not queue_one or not queue_two:
            return True
       
        while queue_one and queue_two:
            if queue_one[-1] != queue_two[-1]:
                return False
            queue_one.pop()
            queue_two.pop()
       
        return True
 
 
#Two Two-Pointers
#O(M+N) Time, O(M+N) Space
class Solution:
    def areSentencesSimilar(self, sentence1: str, sentence2: str) -> bool:
        sentence1 = sentence1.split()
        sentence2 = sentence2.split()
       
        one_left, one_right = 0, len(sentence1) - 1
        two_left, two_right = 0, len(sentence2) - 1
       
        while one_left <= one_right and two_left <= two_right:
            if sentence1[one_left] != sentence2[two_left]:
                break
            one_left += 1
            two_left += 1
 
        if one_left > one_right or two_left > two_right:
            return True
       
        while one_right >= one_left and two_right >= two_left:
            if sentence1[one_right] != sentence2[two_right]:
                return False
            one_right -= 1
            two_right -= 1
       
        return True

Problem Explanation


Two Queues

Based on the given conditions, two words are similar if we can insert more words in the middle of the shorter string to become the longer one.

"My              Haley" ->"My name is Haley" 

 

Another way of thinking about this is that two strings are similar if we can delete a suffix and/or prefix to isolate the words that were inserted.

This would be the case if the entire smaller string was deleted for nonnull insertions, or both strings were deleted from a null insertion meaning the two strings are actually the same.

"My Haley"  "My name is Haley

 

We can solve this by pushing the words from the two input strings into two queues.

By doing this, we will be able to pop off matching prefixes from the left side of the queues and pop off matching suffixes from the right side.

If we are able to delete each word from one of the queues in the process then that means the shorter string was either just a prefix of the larger one, was just a suffix of the larger one, had a matching prefix and suffix to the larger one, or was exactly the same string as the larger one.

Otherwise, it wouldn't be possible to inject words into the smaller one for it to become the larger one.


Let's start by initializing our two queues with both sentence strings.

We'll need to split the sentences into arrays of words so that we are popping matching words instead of matching characters.

        queue_one = collections.deque(sentence1.split())
        queue_two = collections.deque(sentence2.split())

 

Now we will remove matching prefixes while we haven't deleted an entire queue yet.

        while queue_one and queue_two:

 

If the leftmost words do not match, we no longer have a matching prefix so we will break.

            if queue_one[0] != queue_two[0]:
                break

 

Otherwise, we will pop the current matching prefix word from the beginning of both queues.

            queue_one.popleft()
            queue_two.popleft()

 

Once we have broken from the loop, we either broke because one of the queues was empty or the prefixes no longer match.

If it was the former scenario, then that means the entire smaller input string was just a prefix of the larger one or both input strings were the same.

If this was the case, we will return true.

         if not queue_one or not queue_two:
            return True

 

Otherwise, we will now pop off matching suffixes from the ends of both queues.

However, in this case, if we find a mismatched element then we will return false.  

We just searched for matching prefixes within the previous loop and found a mismatch so if we find a mismatch in suffixes, we now know that these are not similar strings.

        while queue_one and queue_two:
            if queue_one[-1] != queue_two[-1]:
                return False

 

If these are matching suffixes, however, we will continue popping these matching words off the right side.

            queue_one.pop()
            queue_two.pop()

 

If we broke from the loop this time and didn't return false, then that means we deleted an entire queue so the entire smaller input string was just a suffix of the larger one or both input strings were the same.

In this case, we'll return true.

        return True



Two Pointers

A two-pointer approach can be utilized in place of two queues.

This approach follows the same exact logic as the queue approach, but here we will know if we deleted an entire sentence when a sentence's pointers overlap.


Let's start by splitting the sentences into arrays of words so that we are comparing elements of matching words instead of matching characters.

        sentence1 = sentence1.split()
        sentence2 = sentence2.split()

 

We'll then initialize two pointers for each sentence, one at the beginning and one at the end.

        one_left, one_right = 0, len(sentence1) - 1
        two_left, two_right = 0, len(sentence2) - 1

 

Now we will exclude matching prefixes while we haven't had two pointers overlap.

        while one_left <= one_right and two_left <= two_right:

 

If the leftmost words do not match, we no longer have a matching prefix so we will break.

            if sentence1[one_left] != sentence2[two_left]:
                break

 

Otherwise, we will increment both left pointers.

            one_left += 1
            two_left += 1

 

Once we have broken from the loop, we either broke because one of the sentence's pointers overlapped or the prefixes no longer match.

If it was the former scenario, then that means the entire smaller input string was just a prefix of the larger one or both input strings were the same.

If this was the case, we will return true.

        if one_left > one_right or two_left > two_right:
            return True

 

Otherwise, we will now exclude matching suffixes from the ends of both sentences.

However, in this case, if we find a mismatched element then we will return false.  

We just searched for matching prefixes within the previous loop and found a mismatch so if we find a mismatch in suffixes, we now know that these are not similar strings.

        while one_right >= one_left and two_right >= two_left:
            if sentence1[one_right] != sentence2[two_right]:
                return False

 

If these are matching suffixes, however, we will continue decrementing our right pointers.

            one_right -= 1
            two_right -= 1

 

If we broke from the loop this time and didn't return false, then that means we had an overlap so the entire smaller input string was just a suffix of the larger one or both input strings were the same.

In this case, we'll return true.

        return True