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 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 sentencet
and 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
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
takesO(n)
wheren
is 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 hasm
words, we generaten^m
different sentences. Since we're consideringn
as 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)
wherem
is 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)}
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
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!