Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

1258. Synonymous Sentences - Leetcode Solution

Problem Description

The "Synonymous Sentences" problem asks you to generate all possible sentences by replacing words in a given sentence with their synonyms, as specified by a list of synonym pairs.

  • You are given a list of synonym pairs, where each pair contains two words that are considered synonyms of each other.
  • You are also given a sentence, represented as a string of words separated by spaces.
  • For each word in the sentence, you can replace it with any of its synonyms (including itself), and you must generate all possible sentences that can be formed in this way.
  • The output should be a list of all possible sentences, sorted lexicographically.

Key constraints:

  • Each word can only be replaced by its synonyms (transitively, e.g., if A is a synonym of B, and B of C, then A, B, and C are all synonyms).
  • Words not in any synonym pair remain unchanged.
  • Do not reuse synonyms in ways that break the equivalence classes formed by the pairs.

Thought Process

At first glance, you might consider a brute-force approach: for each word in the sentence, replace it with all possible synonyms, and recursively repeat this for each position. However, this quickly becomes inefficient, especially if synonym groups are large or overlap.

The key insight is that synonym relationships are transitive: if "happy" is a synonym of "joyful" and "joyful" of "cheerful", then all three are synonyms. Thus, we need a way to group all connected synonyms together efficiently. This is a classic use-case for the "Union Find" (Disjoint Set Union) data structure.

Once we've grouped synonyms, for each word in the sentence, we can look up its synonym group and generate all possible replacements. Then, we combine all possibilities using backtracking or recursion.

Solution Approach

Let's break down the steps to solve the problem efficiently:

  1. Build synonym groups:
    • Use Union Find (Disjoint Set Union) to group all words that are synonyms (directly or transitively).
    • This allows us to quickly find all synonyms for a given word.
  2. Map group leaders to the words in their group:
    • After building the groups, create a mapping from each group leader to the set of words in that group.
    • This makes it easy to look up all synonyms for a word.
  3. Process the sentence:
    • Split the sentence into words.
    • For each word, get its synonym group (or just itself if not in any group).
  4. Generate all possible sentences:
    • Use backtracking to try all combinations: for each word position, try each possible synonym.
    • When you reach the end of the sentence, add the generated sentence to the result list.
  5. Sort the results:
    • Return the list of sentences in lexicographical order.

We use hash maps for fast lookups, and the union-find structure for efficient group merging and querying.

Example Walkthrough

Example Input:
Synonyms: [["happy", "joy"], ["sad", "sorrow"], ["joy", "cheerful"]]
Sentence: "I am happy today but was sad yesterday"

  1. Build synonym groups:
    • "happy", "joy", "cheerful" are grouped together.
    • "sad", "sorrow" are grouped together.
  2. Process the sentence:
    • "I" (no synonyms) → ["I"]
    • "am" (no synonyms) → ["am"]
    • "happy" → ["cheerful", "happy", "joy"]
    • "today" (no synonyms) → ["today"]
    • "but" (no synonyms) → ["but"]
    • "was" (no synonyms) → ["was"]
    • "sad" → ["sad", "sorrow"]
    • "yesterday" (no synonyms) → ["yesterday"]
  3. Backtrack and generate sentences:
    • For "happy", try "cheerful", "happy", "joy".
    • For "sad", try "sad", "sorrow".
    • Combine all possibilities (cartesian product): 3 (happy) × 2 (sad) = 6 sentences.
  4. Example outputs (sorted):
    • I am cheerful today but was sad yesterday
    • I am cheerful today but was sorrow yesterday
    • I am happy today but was sad yesterday
    • I am happy today but was sorrow yesterday
    • I am joy today but was sad yesterday
    • I am joy today but was sorrow yesterday

Time and Space Complexity

Brute-force approach:

  • If every word had k synonyms and there are n words, the brute-force approach would generate up to k^n sentences, which is exponential and infeasible for large n or k.
Optimized approach:
  • Building the union-find structure: nearly linear in the number of synonym pairs (O(P) where P is the number of pairs), since union-find with path compression is very fast.
  • For each word, finding its synonym group is O(1) (amortized).
  • Generating all possible sentences: O(S), where S is the product of the number of synonyms for each word (i.e., total number of output sentences).
  • Space: O(U + S), where U is the number of unique words in synonym groups, and S is the number of output sentences.

The bottleneck is the number of possible sentences, which can still be large if many words have many synonyms, but the algorithm is efficient in all other aspects.

Summary

The "Synonymous Sentences" problem is elegantly solved by grouping synonyms using union-find and then generating all possible sentences using backtracking. The main insight is to treat synonym pairs as equivalence classes, allowing us to efficiently look up all synonyms for any word. The final solution is both efficient and clear, leveraging classic data structures and recursive generation techniques to handle all possible combinations.

Code Implementation

class Solution:
    def generateSentences(self, synonyms, text):
        from collections import defaultdict

        parent = {}

        def find(x):
            parent.setdefault(x, x)
            if parent[x] != x:
                parent[x] = find(parent[x])
            return parent[x]

        def union(x, y):
            parent[find(x)] = find(y)

        # Build union-find structure
        for a, b in synonyms:
            union(a, b)

        # Build groupings: leader -> set of words
        groups = defaultdict(set)
        for word in parent:
            leader = find(word)
            groups[leader].add(word)

        # For each word in sentence, get all synonyms (or itself)
        words = text.split()
        options = []
        for w in words:
            if w in parent:
                leader = find(w)
                options.append(sorted(groups[leader]))
            else:
                options.append([w])

        res = []

        def backtrack(idx, path):
            if idx == len(options):
                res.append(' '.join(path))
                return
            for opt in options[idx]:
                backtrack(idx + 1, path + [opt])

        backtrack(0, [])
        res.sort()
        return res
      
class Solution {
public:
    vector<string> generateSentences(vector<vector<string>>& synonyms, string text) {
        unordered_map<string, string> parent;
        function<string(const string&)> find = [&](const string& x) {
            if (!parent.count(x)) parent[x] = x;
            if (parent[x] != x) parent[x] = find(parent[x]);
            return parent[x];
        };
        auto unite = [&](const string& a, const string& b) {
            parent[find(a)] = find(b);
        };
        for (auto& pair : synonyms) {
            unite(pair[0], pair[1]);
        }
        unordered_map<string, set<string>> groups;
        for (auto& it : parent) {
            string leader = find(it.first);
            groups[leader].insert(it.first);
        }
        vector<vector<string>> options;
        istringstream iss(text);
        string word;
        while (iss >> word) {
            if (parent.count(word)) {
                string leader = find(word);
                vector<string> v(groups[leader].begin(), groups[leader].end());
                options.push_back(v);
            } else {
                options.push_back({word});
            }
        }
        vector<string> res;
        vector<string> path;
        function<void(int)> backtrack = [&](int idx) {
            if (idx == options.size()) {
                stringstream ss;
                for (int i = 0; i < path.size(); ++i) {
                    if (i) ss << ' ';
                    ss << path[i];
                }
                res.push_back(ss.str());
                return;
            }
            for (const string& opt : options[idx]) {
                path.push_back(opt);
                backtrack(idx + 1);
                path.pop_back();
            }
        };
        backtrack(0);
        sort(res.begin(), res.end());
        return res;
    }
};
      
class Solution {
    public List<String> generateSentences(List<List<String>> synonyms, String text) {
        Map<String, String> parent = new HashMap<>();
        // Union-Find
        Function<String, String> find = new Function<>() {
            public String apply(String x) {
                parent.putIfAbsent(x, x);
                if (!parent.get(x).equals(x)) {
                    parent.put(x, this.apply(parent.get(x)));
                }
                return parent.get(x);
            }
        };
        // Union
        for (List<String> pair : synonyms) {
            String a = pair.get(0), b = pair.get(1);
            parent.put(find.apply(a), find.apply(b));
        }
        // Build groupings
        Map<String, TreeSet<String>> groups = new HashMap<>();
        for (String word : parent.keySet()) {
            String leader = find.apply(word);
            groups.computeIfAbsent(leader, k -> new TreeSet<>()).add(word);
        }
        // Process sentence
        String[] words = text.split(" ");
        List<List<String>> options = new ArrayList<>();
        for (String w : words) {
            if (parent.containsKey(w)) {
                String leader = find.apply(w);
                options.add(new ArrayList<>(groups.get(leader)));
            } else {
                options.add(Arrays.asList(w));
            }
        }
        List<String> res = new ArrayList<>();
        backtrack(options, 0, new ArrayList<>(), res);
        Collections.sort(res);
        return res;
    }
    private void backtrack(List<List<String>> options, int idx, List<String> path, List<String> res) {
        if (idx == options.size()) {
            res.add(String.join(" ", path));
            return;
        }
        for (String opt : options.get(idx)) {
            path.add(opt);
            backtrack(options, idx + 1, path, res);
            path.remove(path.size() - 1);
        }
    }
}
      
var generateSentences = function(synonyms, text) {
    const parent = {};
    function find(x) {
        if (!(x in parent)) parent[x] = x;
        if (parent[x] !== x)
            parent[x] = find(parent[x]);
        return parent[x];
    }
    function union(x, y) {
        parent[find(x)] = find(y);
    }
    for (let [a, b] of synonyms) {
        union(a, b);
    }
    const groups = {};
    for (let word in parent) {
        let leader = find(word);
        if (!(leader in groups)) groups[leader] = new Set();
        groups[leader].add(word);
    }
    const words = text.split(' ');
    const options = [];
    for (let w of words) {
        if (w in parent) {
            let leader = find(w);
            options.push([...groups[leader]].sort());
        } else {
            options.push([w]);
        }
    }
    const res = [];
    function backtrack(idx, path) {
        if (idx === options.length) {
            res.push(path.join(' '));
            return;
        }
        for (let opt of options[idx]) {
            backtrack(idx + 1, path.concat(opt));
        }
    }
    backtrack(0, []);
    res.sort();
    return res;
};