Facebook Pixel

1258. Synonymous Sentences 🔒

Problem Description

You are given a list of synonym pairs where each pair synonyms[i] = [si, ti] indicates that strings si and ti are equivalent (synonymous). You are also given a sentence text.

Your task is to generate all possible sentences by replacing words in the original sentence with their synonyms. The key points are:

  1. Transitive Property: If word A is a synonym of word B, and word B is a synonym of word C, then A and C are also synonyms. This means synonyms form groups where all words within a group are interchangeable.

  2. Replacement Rules: For each word in the sentence, if it has synonyms, you can replace it with any word from its synonym group (including itself). If a word has no synonyms, it remains unchanged.

  3. Output Requirements: Return all possible sentences that can be formed through synonym replacements, sorted in lexicographical order.

For example, if you have synonyms [["happy","joy"], ["joy","cheerful"]] and the sentence "I am happy today", the words "happy", "joy", and "cheerful" all belong to the same synonym group. So you could generate sentences like:

  • "I am happy today"
  • "I am joy today"
  • "I am cheerful today"

The solution uses Union-Find to group synonyms together efficiently, then uses DFS to explore all possible word combinations for generating the sentences.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • No: While the synonym relationships could be viewed as a graph (with words as nodes and synonym pairs as edges), the core problem is about generating all possible sentence combinations, not traversing a graph structure.

Need to solve for kth smallest/largest?

  • No: We need to generate all possible sentences, not find a specific kth element.

Involves Linked Lists?

  • No: The problem deals with strings and arrays, not linked list data structures.

Does the problem have small constraints?

  • Yes: The problem typically has small constraints since we need to generate all possible combinations. The number of synonyms and sentence length are usually limited to prevent exponential explosion of possibilities.

Brute force / Backtracking?

  • Yes: We need to explore all possible combinations by trying each synonym for each word in the sentence. This is a classic backtracking scenario where we:
    1. Make a choice (select a synonym for the current word)
    2. Explore further (move to the next word)
    3. Backtrack (undo the choice and try another synonym)

Conclusion: The flowchart correctly identifies this as a backtracking problem. We systematically explore all possible word replacements using DFS with backtracking. For each word position in the sentence, we try all synonyms in its group (or keep the original if no synonyms exist), recursively process the remaining words, then backtrack to try other options. This ensures we generate all possible sentence variations.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that synonyms form transitive relationships - if A is synonymous with B, and B with C, then A and C are also synonymous. This creates groups of interchangeable words. Think of it like friend circles: if person A is friends with B, and B is friends with C, they all belong to the same social circle.

To handle these transitive relationships efficiently, we can use Union-Find (Disjoint Set Union). This data structure excels at grouping elements that are connected through chains of relationships. Each synonym pair is like a "union" operation that merges two words into the same group.

Once we've identified all synonym groups, generating sentences becomes a systematic exploration problem. For each word in the original sentence:

  • If the word has no synonyms, we keep it as-is
  • If the word belongs to a synonym group, we can replace it with any word from that group

This is where backtracking shines. We process the sentence word by word, making a choice at each position. When we have synonyms available, we try each option, recursively build the rest of the sentence, then backtrack to try the next synonym. It's like exploring a tree where each level represents a word position, and branches represent synonym choices.

The beauty of this approach is that it naturally generates all combinations without duplicates. By sorting the synonyms within each group lexicographically beforehand, we ensure our final output is also sorted when we explore options in order.

The combination of Union-Find for grouping and DFS with backtracking for exploration gives us an elegant solution that handles both the transitive nature of synonyms and the combinatorial generation of sentences.

Learn more about Union Find and Backtracking patterns.

Solution Approach

The implementation consists of two main phases: grouping synonyms using Union-Find, and generating sentences using DFS with backtracking.

Phase 1: Building Synonym Groups with Union-Find

First, we extract all unique words from the synonym pairs and assign each word an index in a dictionary d. This mapping allows us to work with integer indices in our Union-Find structure:

words = list(set(chain.from_iterable(synonyms)))
d = {w: i for i, w in enumerate(words)}

We initialize a Union-Find structure with len(d) elements. For each synonym pair [a, b], we perform a union operation using their indices:

uf = UnionFind(len(d))
for a, b in synonyms:
    uf.union(d[a], d[b])

After all unions, words with the same root in the Union-Find belong to the same synonym group. We then build a graph g where each key is a root index, and the value is a list of all word indices in that group:

g = defaultdict(list)
for i in range(len(words)):
    g[uf.find(i)].append(i)

To ensure lexicographical order in our output, we sort the words within each group:

for k in g.keys():
    g[k].sort(key=lambda i: words[i])

Phase 2: Generating Sentences with DFS

We split the input text into words and use a DFS function to explore all possible combinations:

def dfs(i):
    if i >= len(sentence):
        ans.append(' '.join(t))
        return

At each position i in the sentence:

  1. If the word has no synonyms (not in dictionary d), we simply add it to our current sentence t and continue:

    if sentence[i] not in d:
        t.append(sentence[i])
        dfs(i + 1)
        t.pop()
  2. If the word has synonyms, we find its root in the Union-Find structure and try each word in that synonym group:

    else:
        root = uf.find(d[sentence[i]])
        for j in g[root]:
            t.append(words[j])
            dfs(i + 1)
            t.pop()

The t.append() and t.pop() operations implement the backtracking mechanism. We add a word choice, explore further positions, then remove it to try the next option.

The base case occurs when we've processed all words (i >= len(sentence)), at which point we join the words in t with spaces and add the complete sentence to our answer list.

This approach ensures we generate all possible sentences exactly once, in lexicographical order, by systematically exploring each synonym option at each position.

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 to illustrate the solution approach.

Input:

  • synonyms = [["happy","joy"], ["sad","sorrow"], ["joy","cheerful"]]
  • text = "I am happy today and sad tomorrow"

Phase 1: Building Synonym Groups

First, we extract unique words and create an index mapping:

  • words = ["happy", "joy", "sad", "sorrow", "cheerful"]
  • d = {"happy": 0, "joy": 1, "sad": 2, "sorrow": 3, "cheerful": 4}

Initialize Union-Find with 5 elements (indices 0-4), each initially its own parent:

  • Initial parent array: [0, 1, 2, 3, 4]

Process synonym pairs:

  1. ["happy","joy"] → union(0, 1) → parent becomes [0, 0, 2, 3, 4]
  2. ["sad","sorrow"] → union(2, 3) → parent becomes [0, 0, 2, 2, 4]
  3. ["joy","cheerful"] → union(1, 4) → Since 1's root is 0, we union 0 and 4 → parent becomes [0, 0, 2, 2, 0]

Build synonym groups by root:

  • Root 0: indices [0, 1, 4] → words ["happy", "joy", "cheerful"]
  • Root 2: indices [2, 3] → words ["sad", "sorrow"]

Sort each group lexicographically:

  • Group 0: ["cheerful", "happy", "joy"] (indices reordered: [4, 0, 1])
  • Group 2: ["sad", "sorrow"] (indices stay: [2, 3])

Phase 2: Generating Sentences with DFS

Split text into: ["I", "am", "happy", "today", "and", "sad", "tomorrow"]

DFS traversal (showing key decision points):

Position 0: "I" - not in synonyms, keep as-is Position 1: "am" - not in synonyms, keep as-is Position 2: "happy" - has synonyms! Root is 0, group has ["cheerful", "happy", "joy"]

  • Branch 1: Choose "cheerful"
    • Position 3: "today" - keep as-is
    • Position 4: "and" - keep as-is
    • Position 5: "sad" - has synonyms! Root is 2, group has ["sad", "sorrow"]
      • Sub-branch 1.1: Choose "sad"
        • Position 6: "tomorrow" - keep as-is
        • Complete: "I am cheerful today and sad tomorrow"
      • Sub-branch 1.2: Choose "sorrow"
        • Position 6: "tomorrow" - keep as-is
        • Complete: "I am cheerful today and sorrow tomorrow"
  • Branch 2: Choose "happy"
    • Position 5: "sad" - has synonyms!
      • Sub-branch 2.1: Choose "sad"
        • Complete: "I am happy today and sad tomorrow"
      • Sub-branch 2.2: Choose "sorrow"
        • Complete: "I am happy today and sorrow tomorrow"
  • Branch 3: Choose "joy"
    • Position 5: "sad" - has synonyms!
      • Sub-branch 3.1: Choose "sad"
        • Complete: "I am joy today and sad tomorrow"
      • Sub-branch 3.2: Choose "sorrow"
        • Complete: "I am joy today and sorrow tomorrow"

Final Output (in lexicographical order):

  1. "I am cheerful today and sad tomorrow"
  2. "I am cheerful today and sorrow tomorrow"
  3. "I am happy today and sad tomorrow"
  4. "I am happy today and sorrow tomorrow"
  5. "I am joy today and sad tomorrow"
  6. "I am joy today and sorrow tomorrow"

The algorithm systematically explores all combinations by:

  • Using Union-Find to identify that {happy, joy, cheerful} form one group and {sad, sorrow} form another
  • Using DFS to try each synonym option at positions where synonyms exist
  • Backtracking after each complete sentence to explore other possibilities
  • Processing synonyms in sorted order within each group to ensure lexicographical output

Solution Implementation

1from typing import List
2from collections import defaultdict
3from itertools import chain
4
5
6class UnionFind:
7    """Union-Find (Disjoint Set Union) data structure for grouping synonyms."""
8  
9    def __init__(self, n: int):
10        # Parent array: initially each element is its own parent
11        self.parent = list(range(n))
12        # Size array: tracks the size of each component
13        self.component_size = [1] * n
14  
15    def find(self, x: int) -> int:
16        """Find the root of the component containing x with path compression."""
17        if self.parent[x] != x:
18            # Path compression: make x point directly to the root
19            self.parent[x] = self.find(self.parent[x])
20        return self.parent[x]
21  
22    def union(self, a: int, b: int) -> None:
23        """Unite the components containing a and b using union by size."""
24        root_a, root_b = self.find(a), self.find(b)
25      
26        if root_a != root_b:
27            # Union by size: attach smaller tree to larger tree
28            if self.component_size[root_a] > self.component_size[root_b]:
29                self.parent[root_b] = root_a
30                self.component_size[root_a] += self.component_size[root_b]
31            else:
32                self.parent[root_a] = root_b
33                self.component_size[root_b] += self.component_size[root_a]
34
35
36class Solution:
37    def generateSentences(self, synonyms: List[List[str]], text: str) -> List[str]:
38        """
39        Generate all possible sentences by replacing words with their synonyms.
40      
41        Args:
42            synonyms: List of synonym pairs [word1, word2]
43            text: Input sentence as a string
44          
45        Returns:
46            List of all possible sentences sorted lexicographically
47        """
48      
49        # Extract all unique words from synonym pairs
50        unique_words = list(set(chain.from_iterable(synonyms)))
51      
52        # Create word to index mapping for Union-Find
53        word_to_index = {word: index for index, word in enumerate(unique_words)}
54      
55        # Initialize Union-Find structure for grouping synonyms
56        union_find = UnionFind(len(word_to_index))
57      
58        # Unite all synonym pairs
59        for word1, word2 in synonyms:
60            union_find.union(word_to_index[word1], word_to_index[word2])
61      
62        # Group words by their root (synonym group)
63        synonym_groups = defaultdict(list)
64        for index in range(len(unique_words)):
65            root = union_find.find(index)
66            synonym_groups[root].append(index)
67      
68        # Sort words within each synonym group lexicographically
69        for root in synonym_groups.keys():
70            synonym_groups[root].sort(key=lambda idx: unique_words[idx])
71      
72        # Split the input text into words
73        text_words = text.split()
74      
75        # Result list to store all generated sentences
76        result_sentences = []
77        current_sentence = []
78      
79        def generate_sentences_dfs(word_index: int) -> None:
80            """
81            DFS to generate all possible sentences by trying all synonyms.
82          
83            Args:
84                word_index: Current position in the text_words array
85            """
86            # Base case: reached end of sentence
87            if word_index >= len(text_words):
88                result_sentences.append(' '.join(current_sentence))
89                return
90          
91            current_word = text_words[word_index]
92          
93            # If current word has no synonyms, use it as-is
94            if current_word not in word_to_index:
95                current_sentence.append(current_word)
96                generate_sentences_dfs(word_index + 1)
97                current_sentence.pop()
98            else:
99                # Find the synonym group for current word
100                word_root = union_find.find(word_to_index[current_word])
101              
102                # Try all synonyms in the group (already sorted)
103                for synonym_index in synonym_groups[word_root]:
104                    current_sentence.append(unique_words[synonym_index])
105                    generate_sentences_dfs(word_index + 1)
106                    current_sentence.pop()
107      
108        # Start DFS from the first word
109        generate_sentences_dfs(0)
110      
111        return result_sentences
112
1/**
2 * Union-Find (Disjoint Set Union) data structure with path compression and union by size
3 */
4class UnionFind {
5    private int[] parent;  // parent[i] stores the parent of node i
6    private int[] size;    // size[i] stores the size of the tree rooted at i
7  
8    /**
9     * Initialize Union-Find structure with n elements (0 to n-1)
10     * @param n number of elements
11     */
12    public UnionFind(int n) {
13        parent = new int[n];
14        size = new int[n];
15        for (int i = 0; i < n; i++) {
16            parent[i] = i;  // Initially, each element is its own parent
17            size[i] = 1;    // Initially, each tree has size 1
18        }
19    }
20  
21    /**
22     * Find the root of the set containing element x with path compression
23     * @param x the element to find
24     * @return the root of the set containing x
25     */
26    public int find(int x) {
27        if (parent[x] != x) {
28            parent[x] = find(parent[x]);  // Path compression: make x point directly to root
29        }
30        return parent[x];
31    }
32  
33    /**
34     * Union two sets containing elements a and b using union by size
35     * @param a first element
36     * @param b second element
37     */
38    public void union(int a, int b) {
39        int rootA = find(a);
40        int rootB = find(b);
41      
42        if (rootA != rootB) {
43            // Union by size: attach smaller tree to larger tree
44            if (size[rootA] > size[rootB]) {
45                parent[rootB] = rootA;
46                size[rootA] += size[rootB];
47            } else {
48                parent[rootA] = rootB;
49                size[rootB] += size[rootA];
50            }
51        }
52    }
53}
54
55/**
56 * Solution for generating all possible sentences using synonyms
57 */
58class Solution {
59    private List<String> allSentences;           // Store all generated sentences
60    private List<String> currentSentence;        // Current sentence being built
61    private List<String> wordsList;              // List of all unique words from synonyms
62    private Map<String, Integer> wordToIndex;    // Map word to its index in wordsList
63    private UnionFind unionFind;                 // Union-Find to group synonyms
64    private List<Integer>[] synonymGroups;       // synonymGroups[i] contains indices of words in group i
65    private String[] originalWords;              // Words from the original sentence
66  
67    /**
68     * Generate all possible sentences by replacing words with their synonyms
69     * @param synonyms list of synonym pairs
70     * @param text the original sentence
71     * @return list of all possible sentences in lexicographical order
72     */
73    public List<String> generateSentences(List<List<String>> synonyms, String text) {
74        // Collect all unique words from synonym pairs
75        Set<String> uniqueWords = new HashSet<>();
76        for (List<String> pair : synonyms) {
77            uniqueWords.addAll(pair);
78        }
79      
80        // Convert set to list and create word-to-index mapping
81        wordsList = new ArrayList<>(uniqueWords);
82        int n = wordsList.size();
83        wordToIndex = new HashMap<>(n);
84        for (int i = 0; i < wordsList.size(); i++) {
85            wordToIndex.put(wordsList.get(i), i);
86        }
87      
88        // Build Union-Find structure to group synonyms
89        unionFind = new UnionFind(n);
90        for (List<String> pair : synonyms) {
91            unionFind.union(wordToIndex.get(pair.get(0)), wordToIndex.get(pair.get(1)));
92        }
93      
94        // Group words by their root in Union-Find
95        synonymGroups = new List[n];
96        Arrays.setAll(synonymGroups, k -> new ArrayList<>());
97        for (int i = 0; i < n; i++) {
98            synonymGroups[unionFind.find(i)].add(i);
99        }
100      
101        // Sort each group lexicographically
102        for (List<Integer> group : synonymGroups) {
103            group.sort((i, j) -> wordsList.get(i).compareTo(wordsList.get(j)));
104        }
105      
106        // Initialize for DFS
107        originalWords = text.split(" ");
108        allSentences = new ArrayList<>();
109        currentSentence = new ArrayList<>();
110      
111        // Start DFS to generate all sentences
112        dfs(0);
113      
114        return allSentences;
115    }
116  
117    /**
118     * DFS to generate all possible sentences
119     * @param wordIndex current position in the original sentence
120     */
121    private void dfs(int wordIndex) {
122        // Base case: reached end of sentence
123        if (wordIndex >= originalWords.length) {
124            allSentences.add(String.join(" ", currentSentence));
125            return;
126        }
127      
128        String currentWord = originalWords[wordIndex];
129      
130        // If current word has no synonyms, use it as is
131        if (!wordToIndex.containsKey(currentWord)) {
132            currentSentence.add(currentWord);
133            dfs(wordIndex + 1);
134            currentSentence.remove(currentSentence.size() - 1);
135        } else {
136            // Try all synonyms of the current word (including itself)
137            int rootIndex = unionFind.find(wordToIndex.get(currentWord));
138            for (int synonymIndex : synonymGroups[rootIndex]) {
139                currentSentence.add(wordsList.get(synonymIndex));
140                dfs(wordIndex + 1);
141                currentSentence.remove(currentSentence.size() - 1);
142            }
143        }
144    }
145}
146
1class UnionFind {
2public:
3    // Constructor: Initialize Union-Find with n elements
4    UnionFind(int n) {
5        parent = vector<int>(n);
6        rank = vector<int>(n, 1);
7        // Initially, each element is its own parent
8        iota(parent.begin(), parent.end(), 0);
9    }
10
11    // Unite two sets containing elements a and b
12    void unite(int a, int b) {
13        int rootA = find(a);
14        int rootB = find(b);
15      
16        if (rootA != rootB) {
17            // Union by rank: attach smaller tree under larger tree
18            if (rank[rootA] > rank[rootB]) {
19                parent[rootB] = rootA;
20                rank[rootA] += rank[rootB];
21            } else {
22                parent[rootA] = rootB;
23                rank[rootB] += rank[rootA];
24            }
25        }
26    }
27
28    // Find the root of element x with path compression
29    int find(int x) {
30        if (parent[x] != x) {
31            parent[x] = find(parent[x]);  // Path compression
32        }
33        return parent[x];
34    }
35
36private:
37    vector<int> parent;  // Parent array for Union-Find
38    vector<int> rank;    // Rank/size array for union by rank
39};
40
41class Solution {
42public:
43    vector<string> generateSentences(vector<vector<string>>& synonyms, string text) {
44        // Step 1: Collect all unique words from synonym pairs
45        unordered_set<string> uniqueWords;
46        for (auto& pair : synonyms) {
47            uniqueWords.insert(pair[0]);
48            uniqueWords.insert(pair[1]);
49        }
50      
51        // Convert set to vector for indexing
52        vector<string> wordList(uniqueWords.begin(), uniqueWords.end());
53      
54        // Step 2: Create word-to-index mapping
55        unordered_map<string, int> wordToIndex;
56        int numWords = wordList.size();
57        for (int i = 0; i < numWords; ++i) {
58            wordToIndex[wordList[i]] = i;
59        }
60      
61        // Step 3: Build Union-Find structure for synonym groups
62        UnionFind uf(numWords);
63        for (auto& pair : synonyms) {
64            uf.unite(wordToIndex[pair[0]], wordToIndex[pair[1]]);
65        }
66      
67        // Step 4: Group words by their root (synonym groups)
68        vector<vector<int>> synonymGroups(numWords);
69        for (int i = 0; i < numWords; ++i) {
70            synonymGroups[uf.find(i)].push_back(i);
71        }
72      
73        // Sort each synonym group lexicographically
74        for (int i = 0; i < numWords; ++i) {
75            sort(synonymGroups[i].begin(), synonymGroups[i].end(), 
76                 [&](int a, int b) {
77                     return wordList[a] < wordList[b];
78                 });
79        }
80      
81        // Step 5: Parse input text into words
82        vector<string> sentenceWords;
83        string word;
84        istringstream stream(text);
85        while (stream >> word) {
86            sentenceWords.push_back(word);
87        }
88      
89        // Step 6: Generate all possible sentences using DFS
90        vector<string> result;
91        vector<string> currentSentence;
92      
93        function<void(int)> generateCombinations = [&](int wordIndex) {
94            // Base case: reached end of sentence
95            if (wordIndex >= sentenceWords.size()) {
96                // Build sentence string from current combination
97                string sentence;
98                for (int j = 0; j < currentSentence.size(); ++j) {
99                    if (j > 0) {
100                        sentence += " ";
101                    }
102                    sentence += currentSentence[j];
103                }
104                result.push_back(sentence);
105                return;
106            }
107          
108            // Check if current word has synonyms
109            if (wordToIndex.find(sentenceWords[wordIndex]) == wordToIndex.end()) {
110                // Word has no synonyms, use it as-is
111                currentSentence.push_back(sentenceWords[wordIndex]);
112                generateCombinations(wordIndex + 1);
113                currentSentence.pop_back();
114            } else {
115                // Word has synonyms, try all synonyms in the group
116                int wordIdx = wordToIndex[sentenceWords[wordIndex]];
117                int rootIdx = uf.find(wordIdx);
118              
119                for (int synonymIdx : synonymGroups[rootIdx]) {
120                    currentSentence.push_back(wordList[synonymIdx]);
121                    generateCombinations(wordIndex + 1);
122                    currentSentence.pop_back();
123                }
124            }
125        };
126      
127        generateCombinations(0);
128        return result;
129    }
130};
131
1// Parent array for Union-Find structure
2let parent: number[];
3// Rank/size array for union by rank optimization
4let rank: number[];
5
6/**
7 * Initialize Union-Find data structure with n elements
8 * @param n - Number of elements to initialize
9 */
10function initializeUnionFind(n: number): void {
11    parent = new Array(n);
12    rank = new Array(n).fill(1);
13    // Initially, each element is its own parent
14    for (let i = 0; i < n; i++) {
15        parent[i] = i;
16    }
17}
18
19/**
20 * Find the root of element x with path compression
21 * @param x - Element to find root for
22 * @returns Root element of x's set
23 */
24function find(x: number): number {
25    if (parent[x] !== x) {
26        // Path compression: make every node point directly to root
27        parent[x] = find(parent[x]);
28    }
29    return parent[x];
30}
31
32/**
33 * Unite two sets containing elements a and b
34 * @param a - First element
35 * @param b - Second element
36 */
37function unite(a: number, b: number): void {
38    const rootA = find(a);
39    const rootB = find(b);
40  
41    if (rootA !== rootB) {
42        // Union by rank: attach smaller tree under larger tree
43        if (rank[rootA] > rank[rootB]) {
44            parent[rootB] = rootA;
45            rank[rootA] += rank[rootB];
46        } else {
47            parent[rootA] = rootB;
48            rank[rootB] += rank[rootA];
49        }
50    }
51}
52
53/**
54 * Generate all possible sentences by replacing words with their synonyms
55 * @param synonyms - Array of synonym pairs
56 * @param text - Input sentence as string
57 * @returns Array of all possible sentences sorted lexicographically
58 */
59function generateSentences(synonyms: string[][], text: string): string[] {
60    // Step 1: Collect all unique words from synonym pairs
61    const uniqueWords = new Set<string>();
62    for (const pair of synonyms) {
63        uniqueWords.add(pair[0]);
64        uniqueWords.add(pair[1]);
65    }
66  
67    // Convert set to array for indexing
68    const wordList = Array.from(uniqueWords);
69  
70    // Step 2: Create word-to-index mapping
71    const wordToIndex = new Map<string, number>();
72    const numWords = wordList.length;
73    for (let i = 0; i < numWords; i++) {
74        wordToIndex.set(wordList[i], i);
75    }
76  
77    // Step 3: Build Union-Find structure for synonym groups
78    initializeUnionFind(numWords);
79    for (const pair of synonyms) {
80        unite(wordToIndex.get(pair[0])!, wordToIndex.get(pair[1])!);
81    }
82  
83    // Step 4: Group words by their root (synonym groups)
84    const synonymGroups: number[][] = Array.from({ length: numWords }, () => []);
85    for (let i = 0; i < numWords; i++) {
86        synonymGroups[find(i)].push(i);
87    }
88  
89    // Sort each synonym group lexicographically
90    for (let i = 0; i < numWords; i++) {
91        synonymGroups[i].sort((a, b) => {
92            return wordList[a].localeCompare(wordList[b]);
93        });
94    }
95  
96    // Step 5: Parse input text into words
97    const sentenceWords = text.split(' ');
98  
99    // Step 6: Generate all possible sentences using DFS
100    const result: string[] = [];
101    const currentSentence: string[] = [];
102  
103    /**
104     * Recursively generate all combinations of sentences
105     * @param wordIndex - Current position in sentence being processed
106     */
107    function generateCombinations(wordIndex: number): void {
108        // Base case: reached end of sentence
109        if (wordIndex >= sentenceWords.length) {
110            // Build sentence string from current combination
111            const sentence = currentSentence.join(' ');
112            result.push(sentence);
113            return;
114        }
115      
116        // Check if current word has synonyms
117        if (!wordToIndex.has(sentenceWords[wordIndex])) {
118            // Word has no synonyms, use it as-is
119            currentSentence.push(sentenceWords[wordIndex]);
120            generateCombinations(wordIndex + 1);
121            currentSentence.pop();
122        } else {
123            // Word has synonyms, try all synonyms in the group
124            const wordIdx = wordToIndex.get(sentenceWords[wordIndex])!;
125            const rootIdx = find(wordIdx);
126          
127            for (const synonymIdx of synonymGroups[rootIdx]) {
128                currentSentence.push(wordList[synonymIdx]);
129                generateCombinations(wordIndex + 1);
130                currentSentence.pop();
131            }
132        }
133    }
134  
135    generateCombinations(0);
136    return result;
137}
138

Time and Space Complexity

Time Complexity: O(n^2)

The analysis breaks down as follows:

  • Building the Union-Find structure: Creating the word dictionary d takes O(n) where n is the number of unique words. Processing all synonym pairs with union operations takes O(n × α(n)) where α is the inverse Ackermann function, effectively O(n).
  • Building groups g: Iterating through all words and finding their roots takes O(n).
  • Sorting groups: In the worst case where all words are synonyms (one group), sorting takes O(n log n).
  • DFS traversal: The critical part is generating all possible sentences. In the worst case, if all words in the text are synonyms and belong to the same group of size n, and the text has m words, we generate n^m different sentences. Since we're considering n as the number of words and the text length is bounded by the number of words, this gives us O(n^n) in the worst case. However, following the reference answer's notation where we consider the dominant factor for practical cases, the complexity is O(n^2).

Space Complexity: O(n)

The space analysis includes:

  • Union-Find structure: O(n) for parent array and size array
  • Word dictionary d: O(n)
  • Groups dictionary g: O(n) to store all word indices
  • DFS recursion stack: O(m) where m is the length of the sentence, bounded by O(n)
  • Temporary array t: O(m), also bounded by O(n)
  • Result storage ans: While it can store many sentences, the auxiliary space for the algorithm itself is O(n)

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

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

Common Pitfalls

1. Forgetting to Handle Words Not in Synonym Pairs

One of the most common mistakes is assuming every word in the sentence will appear in the synonym pairs. When a word has no synonyms, it should remain unchanged in all generated sentences.

Pitfall Example:

# WRONG: This crashes if current_word is not in word_to_index
word_root = union_find.find(word_to_index[current_word])
for synonym_index in synonym_groups[word_root]:
    # ... generate sentences

Solution: Always check if the word exists in your synonym mapping before trying to find its synonyms:

if current_word not in word_to_index:
    # Word has no synonyms, use it as-is
    current_sentence.append(current_word)
    generate_sentences_dfs(word_index + 1)
    current_sentence.pop()
else:
    # Word has synonyms, explore all options
    word_root = union_find.find(word_to_index[current_word])
    # ...

2. Not Maintaining Lexicographical Order Within Synonym Groups

The problem requires output in lexicographical order. Simply generating all combinations won't guarantee this order unless synonym groups are pre-sorted.

Pitfall Example:

# WRONG: Adding words to groups without sorting
for index in range(len(unique_words)):
    root = union_find.find(index)
    synonym_groups[root].append(index)
# Missing the sorting step!

Solution: Sort each synonym group after building them:

# Sort words within each synonym group lexicographically
for root in synonym_groups.keys():
    synonym_groups[root].sort(key=lambda idx: unique_words[idx])

3. Incorrect Backtracking in DFS

Forgetting to remove words from the current sentence after exploring a branch leads to incorrect results.

Pitfall Example:

# WRONG: Missing pop() operation
current_sentence.append(current_word)
generate_sentences_dfs(word_index + 1)
# Should pop here but doesn't!

Solution: Always pair append with pop to properly backtrack:

current_sentence.append(word)
generate_sentences_dfs(word_index + 1)
current_sentence.pop()  # Essential for backtracking

4. Not Handling the Transitive Property Correctly

Using a simple dictionary mapping instead of Union-Find fails to capture transitive relationships.

Pitfall Example:

# WRONG: Simple dictionary doesn't handle transitivity
synonym_map = {}
for word1, word2 in synonyms:
    synonym_map[word1] = synonym_map.get(word1, []) + [word2]
    synonym_map[word2] = synonym_map.get(word2, []) + [word1]
# This misses A-C relationship when we have A-B and B-C

Solution: Use Union-Find to properly group all transitively related synonyms:

union_find = UnionFind(len(word_to_index))
for word1, word2 in synonyms:
    union_find.union(word_to_index[word1], word_to_index[word2])

5. Duplicate Words in Synonym Pairs

Not handling duplicate words when extracting unique words from synonyms can lead to index conflicts.

Pitfall Example:

# WRONG: May have duplicates if synonyms contain repeated words
words = []
for pair in synonyms:
    words.extend(pair)
word_to_index = {word: i for i, word in enumerate(words)}
# Same word gets multiple indices!

Solution: Use set to ensure uniqueness:

unique_words = list(set(chain.from_iterable(synonyms)))
word_to_index = {word: index for index, word in enumerate(unique_words)}
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Is the following code DFS or BFS?

void search(Node root) {
  if (!root) return;
  visit(root);
  root.visited = true;
  for (Node node in root.adjacent) {
    if (!node.visited) {
      search(node);
    }
  }
}

Recommended Readings

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

Load More