1181. Before and After Puzzle

MediumArrayHash TableStringSorting
Leetcode Link

Problem Description

The problem involves creating a set of new phrases called "Before and After puzzles." To form these puzzles, one should merge pairs of given phrases under the condition that the last word of the first phrase is identical to the first word of the second phrase. This merging has to consider the order of the phrases as swapping them yields different results. The expected outcome is to produce a unique set of these merged phrases, without any duplications and sorted in lexicographical (dictionary) order.

Additionally, the constraints are as follows:

  • Each phrase consists only of lowercase letters and spaces.
  • Spaces do not appear at the beginning or the end of a phrase.
  • Phrases do not contain consecutive spaces.

Intuition

The intuition for solving this problem includes the idea that we only need to look at the first and last words of each phrase to determine if a valid "Before and After puzzle" can be formed. To efficiently check for mergeable phrases, we can follow these steps:

  1. Preprocess the input list of phrases by splitting each phrase into its first and last word, noting that these are the crucial points of intersection for merging.
  2. Iterate over the pairs of phrases (excluding pairs of the same phrase) and check if the last word of one phrase matches the first word of another phrase; if they do, we have a potential puzzle.
  3. Merge the phrases by concatenating them, excluding the duplicate word that serves as a joint between them.
  4. Rather than adding all the puzzles into a list sequentially, we can add them to a set to ensure that there are no duplicates.
  5. After considering all the pairs and merging where possible, convert the set of puzzles into a list and sort it lexicographically.

This logical sequence of operations leads us to a solution that not only finds all possible "Before and After puzzles" but also ensures the uniqueness and correct order of the final output.

Learn more about Sorting patterns.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece:

What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)

Solution Approach

The implementation of the solution follows a straightforward, brute-force approach with some optimizations used to process the phrases and store the results. The algorithm, data structures, and pattern used in the reference solution are explained below:

  1. Data Preprocessing: Initially, each phrase is split into its constituent words using split(). Saving only the first and last words of each phrase, since these are the points of potential merging, reduces unnecessary computation. These tuples of (first, last) words are stored in a list called ps.

  2. Iterative Checking: After preprocessing, the code uses nested for loops to iterate over all possible pairs (excluding self-pairing). The condition if i != j ensures no phrase is paired with itself.

  3. Condition Validation and Merging: For each pair, the last word of the first phrase (ps[i][1]) is compared with the first word of the second phrase (ps[j][0]) to check if they are the same. If they are, the two phrases are merged by concatenating the full first phrase with the second one, starting right after its first word. This is done by slicing the second phrase from the length of its first word: phrases[j][len(ps[j][0]):].

  4. Elimination of Duplicates and Sorting: Instead of adding the merged phrases directly to the result list, they are added to a set ans. Using a set automatically removes any duplicate entries, taking advantage of the properties of sets where each element is unique. After processing all pairs, the set is converted into a list (because sets cannot be sorted) which is then sorted using the built-in sorted function to ensure the result is in lexicographic order.

  5. Returning the Result: Finally, the sorted list is returned as the desired output.

The solution uses basic Python data structures: the list for storing phrases and tuples for first and last words, and the set for ensuring unique results. The approach does not use any advanced algorithms but relies on comparing the vital elements (first and last words) of phrases combined with standard operations offered by Python’s data structures.

One could argue that there is room for optimization by using a hash map to store phrases by their first and last words for O(1) lookups, potentially reducing the overall time complexity, but the provided solution is straightforward and sufficient for the problem at hand.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

How does merge sort divide the problem into subproblems?

Example Walkthrough

Let's illustrate the solution approach with a small example. Suppose we are given the following list of phrases:

1["writing code", "code rocks", "daily challenges", "challenges code"]

First, we apply data preprocessing. We need only the first and last words of each phrase:

  • "writing code": first word "writing", last word "code".
  • "code rocks": first word "code", last word "rocks".
  • "daily challenges": first word "daily", last word "challenges".
  • "challenges code": first word "challenges", last word "code".

Then we iterate checking pairs of phrases to merge:

  1. "writing code" (last word "code") can be merged with "code rocks" (first word "code") to form "writing code rocks".
  2. "writing code" (last word "code") can also be merged with "challenges code" (first word "code") to form "writing challenges code".
  3. "daily challenges" (last word "challenges") can be merged with "challenges code" (first word "challenges") to form "daily challenges code".

During merging, we're careful not to pair a phrase with itself and we avoid duplicating the common word.

Next, we add the merged phrases to a set to avoid duplicates:

  • The set will contain {"writing code rocks", "writing challenges code", "daily challenges code"}.

Finally, we sort the results lexicographically and return them:

  • The sorted list will be ["daily challenges code", "writing challenges code", "writing code rocks"].

This is the expected output of the program based on our small example, illustrating the solution approach described in the content.

Solution Implementation

1class Solution:
2    def beforeAndAfterPuzzles(self, phrases):
3        # Store the first and last word of each phrase
4        first_and_last_words = []
5        for phrase in phrases:
6            words = phrase.split()
7            first_and_last_words.append((words[0], words[-1]))
8      
9        num_phrases = len(first_and_last_words)  # The number of phrases
10        results = []  # Store the results
11      
12        # Loop through all combinations of phrases
13        for i in range(num_phrases):
14            for j in range(num_phrases):
15                # Check if i and j are not the same, and the last word of phrase i matches the first word of phrase j
16                if i != j and first_and_last_words[i][1] == first_and_last_words[j][0]:
17                    # Append the combined phrase, removing the duplicate word, to the results
18                    combined_phrase = phrases[i] + phrases[j][len(first_and_last_words[j][0]):]
19                    results.append(combined_phrase)
20                  
21        # Sort the results and remove duplicates
22        return sorted(set(results))
23
1import java.util.*;
2
3class Solution {
4    public List<String> beforeAndAfterPuzzles(String[] phrases) {
5        // Determine the number of phrases.
6        int numPhrases = phrases.length;
7      
8        // This array will hold the first and last words of each phrase.
9        String[][] firstLastWords = new String[numPhrases][];
10        for (int i = 0; i < numPhrases; ++i) {
11            // Split each phrase into words.
12            String[] words = phrases[i].split(" ");
13            // Store the first and the last word of each phrase.
14            firstLastWords[i] = new String[] {words[0], words[words.length - 1]};
15        }
16      
17        // Use a set to avoid duplicate phrases and to store the final phrases.
18        Set<String> uniquePhrases = new HashSet<>();
19        for (int i = 0; i < numPhrases; ++i) {
20            for (int j = 0; j < numPhrases; ++j) {
21                // Exclude same phrase comparisons and check if last word of i is 
22                // equal to first word of j to form a new phrase.
23                if (i != j && firstLastWords[i][1].equals(firstLastWords[j][0])) {
24                    // Concatenate the phrases by removing the overlapping word.
25                    uniquePhrases.add(phrases[i] + phrases[j].substring(firstLastWords[j][0].length()));
26                }
27            }
28        }
29      
30        // Convert the phrase set into a list.
31        var sortedPhrases = new ArrayList<>(uniquePhrases);
32        // Sort the list in alphabetical order.
33        Collections.sort(sortedPhrases);
34      
35        return sortedPhrases;
36    }
37}
38
1#include <vector>
2#include <string>
3#include <utility>
4#include <unordered_set>
5#include <algorithm>
6using namespace std;
7
8class Solution {
9public:
10    // Function to assemble phrases such that the last word of one phrase is the first word of another.
11    vector<string> beforeAndAfterPuzzles(vector<string>& phrases) {
12        int phraseCount = phrases.size();
13        pair<string, string> firstLastWords[phraseCount];
14
15        // Separate each phrase into first and last words.
16        for (int i = 0; i < phraseCount; ++i) {
17            int firstSpacePosition = phrases[i].find(' ');
18            if (firstSpacePosition == string::npos) {
19                // If the phrase has only one word, both first and last are the same.
20                firstLastWords[i] = {phrases[i], phrases[i]};
21            } else {
22                // Find the last space position to separate the last word.
23                int lastSpacePosition = phrases[i].rfind(' ');
24                firstLastWords[i] = {
25                    phrases[i].substr(0, firstSpacePosition),
26                    phrases[i].substr(lastSpacePosition + 1)
27                };
28            }
29        }
30
31        unordered_set<string> uniquePhrases; // To prevent duplicate phrases.
32
33        // Iterate through each pair and combine phrases where the last word of one
34        // is the first word of another.
35        for (int i = 0; i < phraseCount; ++i) {
36            for (int j = 0; j < phraseCount; ++j) {
37                if (i != j && firstLastWords[i].second == firstLastWords[j].first) {
38                    uniquePhrases.insert(phrases[i] +
39                                         phrases[j].substr(firstLastWords[i].second.size()));
40                }
41            }
42        }
43
44        // Transfer and sort the results into a vector for output.
45        vector<string> result(uniquePhrases.begin(), uniquePhrases.end());
46        sort(result.begin(), result.end());
47        return result;
48    }
49};
50
1function beforeAndAfterPuzzles(phrases: string[]): string[] {
2    // Split each phrase into an array of its first and last words.
3    const firstLastWords: string[][] = [];
4    for (const phrase of phrases) {
5        const words = phrase.split(' ');
6        firstLastWords.push([words[0], words[words.length - 1]]);
7    }
8
9    // Count of phrases to process.
10    const phraseCount = firstLastWords.length;
11
12    // Set to store unique combined phrases.
13    const combinedPhrasesSet: Set<string> = new Set();
14
15    // Loop through each pair of phrases.
16    for (let i = 0; i < phraseCount; ++i) {
17        for (let j = 0; j < phraseCount; ++j) {
18            // Check that we do not combine the same phrase and the last word of one
19            // phrase is the same as the first word of the other.
20            if (i !== j && firstLastWords[i][1] === firstLastWords[j][0]) {
21                // Combine phrases by appending the substring of the second phrase that
22                // excludes the overlapping first word to the first phrase.
23                const combinedPhrase = `${phrases[i]}${phrases[j].substring(firstLastWords[j][0].length)}`;
24
25                // Add the combined phrase to the set to ensure uniqueness.
26                combinedPhrasesSet.add(combinedPhrase);
27            }
28        }
29    }
30
31    // Convert the set to an array and sort it in lexicographical order.
32    return Array.from(combinedPhrasesSet).sort();
33}
34
Not Sure What to Study? Take the 2-min Quiz:

Which of the following is a min heap?

Time and Space Complexity

Time Complexity:

The time complexity of the algorithm is determined by several nested operations.

  1. Splitting phrases into tuples of first and last words: This operation is O(m * n), where m is the average length of a phrase, and n is the total number of phrases. Splitting a string into words is an O(m) operation (since it depends on the length of the phrase), and it is done for each phrase.

  2. Double for loop to compare each phrase with every other phrase: There are n phrases, and each phrase is compared with every other phrase, resulting in O(n^2) comparisons.

  3. Concatenation of phrases when a match is found: The worst-case time complexity of string concatenation in Python can be O(k) where k is the length of the resulting string. Since the length of the phrases can vary, also considering the worst-case scenario where all phrases are concatenated, this step can contribute significantly to the time complexity. However, in practice for large strings, Python's string concatenation is usually more efficient due to optimizations. Therefore, we can consider it to be relatively constant for this analysis.

  4. Removal of the first word from the second phrase when concatenating: This operation has a time complexity of O(m), but since it's only done when a match is found and is part of the concatenation process, its impact is included in the previous step.

  5. Sorting the final list of answers: Sorting a list of r results has a time complexity of O(r log r). The value of r can be at most n * (n - 1) / 2, in the case where each phrase can be combined with every other phrase, which simplifies to O((n^2/2) * log(n^2/2)) = O(n^2 log n).

Considering all the aforementioned steps, the dominant term for the time complexity is the double loop with the potential concatenation of phrases: O(n^2 * k) + O(n^2 log n). The overall time complexity in terms of big-O notation is O(n^2 * k + n^2 log n); however, we usually represent the time complexity with the most significant term, which is O(n^2 * k) if k is significantly large, or O(n^2 log n) if phrase concatenation is not a dominant factor (assuming strings are concatenated efficiently).

Space Complexity:

  1. Intermediate list ps of tuples: It has a space complexity of O(n) since it stores a tuple for each phrase.

  2. The ans list: This list will store the combined phrases. In the worst case, where every phrase can be combined with every other phrase, the number of combinations is n * (n - 1) / 2, leading to a space complexity of O(n^2).

  3. Set used in sorted(set(ans)): Converting the list ans to a set to eliminate duplicates, and then sorting it also requires O(n^2) space in the worst case. The sorted list will also occupy O(n^2) space.

With both the intermediate list and the final answer list considered, the predominant term for space complexity is O(n^2) for storing the potential phrase combinations.

Learn more about how to find time and space complexity quickly using problem constraints.

Fast Track Your Learning with Our Quick Skills Quiz:

Depth first search is equivalent to which of the tree traversal order?


Recommended Readings


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫