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:
-
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.
-
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.
-
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:
- Make a choice (select a synonym for the current word)
- Explore further (move to the next word)
- 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.
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 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 1121/** 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} 1461class 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}; 1311// 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} 138Solution 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:
-
If the word has no synonyms (not in dictionary
d), we simply add it to our current sentencetand continue:if sentence[i] not in d: t.append(sentence[i]) dfs(i + 1) t.pop() -
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 EvaluatorExample 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:
["happy","joy"]→ union(0, 1) → parent becomes[0, 0, 2, 3, 4]["sad","sorrow"]→ union(2, 3) → parent becomes[0, 0, 2, 2, 4]["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"
- Sub-branch 1.1: Choose "sad"
- 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"
- Sub-branch 2.1: Choose "sad"
- Position 5: "sad" - has synonyms!
- 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"
- Sub-branch 3.1: Choose "sad"
- Position 5: "sad" - has synonyms!
Final Output (in lexicographical order):
- "I am cheerful today and sad tomorrow"
- "I am cheerful today and sorrow tomorrow"
- "I am happy today and sad tomorrow"
- "I am happy today and sorrow tomorrow"
- "I am joy today and sad tomorrow"
- "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
Time and Space Complexity
Time Complexity: O(n^2)
The analysis breaks down as follows:
- Building the Union-Find structure: Creating the word dictionary
dtakesO(n)wherenis the number of unique words. Processing all synonym pairs with union operations takesO(n × α(n))whereαis the inverse Ackermann function, effectivelyO(n). - Building groups
g: Iterating through all words and finding their roots takesO(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 hasmwords, we generaten^mdifferent sentences. Since we're consideringnas the number of words and the text length is bounded by the number of words, this gives usO(n^n)in the worst case. However, following the reference answer's notation where we consider the dominant factor for practical cases, the complexity isO(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)wheremis the length of the sentence, bounded byO(n) - Temporary array
t:O(m), also bounded byO(n) - Result storage
ans: While it can store many sentences, the auxiliary space for the algorithm itself isO(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)}
What's the output of running the following function using the following tree as input? 
1def serialize(root): 2 res = [] 3 def dfs(root): 4 if not root: 5 res.append('x') 6 return 7 res.append(root.val) 8 dfs(root.left) 9 dfs(root.right) 10 dfs(root) 11 return ' '.join(res) 121import java.util.StringJoiner; 2 3public static String serialize(Node root) { 4 StringJoiner res = new StringJoiner(" "); 5 serializeDFS(root, res); 6 return res.toString(); 7} 8 9private static void serializeDFS(Node root, StringJoiner result) { 10 if (root == null) { 11 result.add("x"); 12 return; 13 } 14 result.add(Integer.toString(root.val)); 15 serializeDFS(root.left, result); 16 serializeDFS(root.right, result); 17} 181function serialize(root) { 2 let res = []; 3 serialize_dfs(root, res); 4 return res.join(" "); 5} 6 7function serialize_dfs(root, res) { 8 if (!root) { 9 res.push("x"); 10 return; 11 } 12 res.push(root.val); 13 serialize_dfs(root.left, res); 14 serialize_dfs(root.right, res); 15} 16Recommended Readings
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
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
Want a Structured Path to Master System Design Too? Don’t Miss This!