From 105bf77d30a780fe0d1304913f7b026726a8112c Mon Sep 17 00:00:00 2001 From: shreyash3087 Date: Wed, 19 Jun 2024 23:41:07 +0530 Subject: [PATCH] Added Solutions to Leetcode 745 --- dsa-problems/leetcode-problems/0700-0799.md | 2 +- .../0745-prefix-and-suffix-search.md | 653 ++++++++++++++++++ 2 files changed, 654 insertions(+), 1 deletion(-) create mode 100644 dsa-solutions/lc-solutions/0700-0799/0745-prefix-and-suffix-search.md diff --git a/dsa-problems/leetcode-problems/0700-0799.md b/dsa-problems/leetcode-problems/0700-0799.md index 53ea64dea..51af937fa 100644 --- a/dsa-problems/leetcode-problems/0700-0799.md +++ b/dsa-problems/leetcode-problems/0700-0799.md @@ -284,7 +284,7 @@ export const problems = [ "problemName": "745. Prefix and Suffix Search", "difficulty": "Hard", "leetCodeLink": "https://leetcode.com/problems/prefix-and-suffix-search", - "solutionLink": "#" + "solutionLink": "/dsa-solutions/lc-solutions/0700-0799/prefix-and-suffix-search" }, { "problemName": "746. Min Cost Climbing Stairs", diff --git a/dsa-solutions/lc-solutions/0700-0799/0745-prefix-and-suffix-search.md b/dsa-solutions/lc-solutions/0700-0799/0745-prefix-and-suffix-search.md new file mode 100644 index 000000000..0404d42b7 --- /dev/null +++ b/dsa-solutions/lc-solutions/0700-0799/0745-prefix-and-suffix-search.md @@ -0,0 +1,653 @@ +--- +id: prefix-and-suffix-search +title: Prefix and Suffix Search +sidebar_label: 0745 - Prefix and Suffix Search +tags: + - String + - Hash Table + - Depth-First Search +description: "This is a solution to the Prefix and Suffix Search problem on LeetCode." +--- + +## Problem Description + +Design a special dictionary that searches the words in it by a prefix and a suffix. + +Implement the `WordFilter` class: + +- `WordFilter(string[] words)` Initializes the object with the `words` in the dictionary. +- `f(string pref, string suff)` Returns the index of the word in the dictionary, which has the prefix `pref` and the suffix `suff`. If there is more than one valid index, return **the largest** of them. If there is no such word in the dictionary, return `-1`. + +### Examples + +**Example 1:** + +``` +Input +["WordFilter", "f"] +[[["apple"]], ["a", "e"]] +Output +[null, 0] +Explanation +WordFilter wordFilter = new WordFilter(["apple"]); +wordFilter.f("a", "e"); // return 0, because the word at index 0 has prefix = "a" and suffix = "e". +``` + +### Constraints + +- $1 \leq words.length \leq 10^4$ +- $1 \leq words[i].length \leq 7$ +- $1 \leq pref.length, suff.length \leq 7$ +- `words[i]`, pref and suff consist of lowercase English letters only. +- At most $10^4$ calls will be made to the function `f`. + +## Solution for Prefix and Suffix Search + +## Approach 1: Trie + Set Intersection +### Intuition and Algorithm + +We use two tries to separately find all words that match the prefix, plus all words that match the suffix. Then, we try to find the highest-weight element in the intersection of these sets. + +Of course, these sets could still be large, so we might TLE if we aren't careful. + +### Code in Different Languages + + + + + +```cpp +#include +#include + +class TrieNode { +public: + TrieNode* children[26]; + std::unordered_set weight; + TrieNode() { + for (int i = 0; i < 26; i++) { + children[i] = nullptr; + } + } +}; + +class WordFilter { +private: + TrieNode trie1, trie2; +public: + WordFilter(std::vector& words) { + int wt = 0; + for (const std::string& word : words) { + TrieNode* cur = &trie1; + cur->weight.insert(wt); + for (char c : word) { + if (cur->children[c - 'a'] == nullptr) { + cur->children[c - 'a'] = new TrieNode(); + } + cur = cur->children[c - 'a']; + cur->weight.insert(wt); + } + + cur = &trie2; + cur->weight.insert(wt); + for (int j = word.length() - 1; j >= 0; j--) { + char c = word[j]; + if (cur->children[c - 'a'] == nullptr) { + cur->children[c - 'a'] = new TrieNode(); + } + cur = cur->children[c - 'a']; + cur->weight.insert(wt); + } + wt++; + } + } + + int f(std::string prefix, std::string suffix) { + TrieNode* cur1 = &trie1, *cur2 = &trie2; + for (char c : prefix) { + if (cur1->children[c - 'a'] == nullptr) { + return -1; + } + cur1 = cur1->children[c - 'a']; + } + for (int j = suffix.length() - 1; j >= 0; j--) { + char c = suffix[j]; + if (cur2->children[c - 'a'] == nullptr) { + return -1; + } + cur2 = cur2->children[c - 'a']; + } + + int ans = -1; + for (int w1 : cur1->weight) { + if (w1 > ans && cur2->weight.count(w1)) { + ans = w1; + } + } + + return ans; + } +}; + +``` + + + + +```java +class WordFilter { + TrieNode trie1, trie2; + public WordFilter(String[] words) { + trie1 = new TrieNode(); + trie2 = new TrieNode(); + int wt = 0; + for (String word: words) { + char[] ca = word.toCharArray(); + + TrieNode cur = trie1; + cur.weight.add(wt); + for (char letter: ca) { + if (cur.children[letter - 'a'] == null) { + cur.children[letter - 'a'] = new TrieNode(); + } + cur = cur.children[letter - 'a']; + cur.weight.add(wt); + } + + cur = trie2; + cur.weight.add(wt); + for (int j = ca.length - 1; j >= 0; --j) { + char letter = ca[j]; + if (cur.children[letter - 'a'] == null) { + cur.children[letter - 'a'] = new TrieNode(); + } + cur = cur.children[letter - 'a']; + cur.weight.add(wt); + } + wt++; + } + } + + public int f(String prefix, String suffix) { + TrieNode cur1 = trie1, cur2 = trie2; + for (char letter: prefix.toCharArray()) { + if (cur1.children[letter - 'a'] == null) { + return -1; + } + cur1 = cur1.children[letter - 'a']; + } + char[] ca = suffix.toCharArray(); + for (int j = ca.length - 1; j >= 0; --j) { + char letter = ca[j]; + if (cur2.children[letter - 'a'] == null) { + return -1; + } + cur2 = cur2.children[letter - 'a']; + } + + int ans = -1; + for (int w1: cur1.weight) { + if (w1 > ans && cur2.weight.contains(w1)) { + ans = w1; + } + } + + return ans; + } +} + +class TrieNode { + TrieNode[] children; + Set weight; + public TrieNode() { + children = new TrieNode[26]; + weight = new HashSet(); + } +} +``` + + + + + +```python +Trie = lambda: collections.defaultdict(Trie) +WEIGHT = False + +class WordFilter: + def __init__(self, words: List[str]): + self.trie1 = Trie() #prefix + self.trie2 = Trie() #suffix + for weight, word in enumerate(words): + cur = self.trie1 + self.addw(cur, weight) + for letter in word: + cur = cur[letter] + self.addw(cur, weight) + + cur = self.trie2 + self.addw(cur, weight) + for letter in word[::-1]: + cur = cur[letter] + self.addw(cur, weight) + + def addw(self, node, w): + if WEIGHT not in node: + node[WEIGHT] = {w} + else: + node[WEIGHT].add(w) + + def f(self, pref: str, suff: str) -> int: + cur1 = self.trie1 + for letter in pref: + if letter not in cur1: + return -1 + cur1 = cur1[letter] + + cur2 = self.trie2 + for letter in suff[::-1]: + if letter not in cur2: r + eturn -1 + cur2 = cur2[letter] + + return max(cur1[WEIGHT] & cur2[WEIGHT], default=-1) +``` + + + +### Complexity Analysis + +#### Time Complexity: $O(NK+Q(N+K))$ + +> **Reason**: where `N` is the number of words, `K` is the maximum length of a word, and Q is the number of queries. If we use memoization in our solution, we could produce tighter bounds for this complexity, as the complex queries are somewhat disjoint. + +#### Space Complexity: $O(NK)$ + +> **Reason**: the size of the tries. + +## Approach 2: Paired Trie +### Intuition and Algorithm + +Say we are inserting the word `apple`. We could insert `('a', 'e'), ('p', 'l'), ('p', 'p'), ('l', 'p'), ('e', 'a')` into our trie. Then, if we had equal length queries like `prefix = "ap", suffix = "le"`, we could find the node `trie['a', 'e']['p', 'l']` in our trie. This seems promising. + +What about queries that aren't equal? We should just insert them like normal. For example, to capture a case like `prefix = "app", suffix = "e"`, we could create nodes `trie['a', 'e']['p', None]['p', None]`. + +After inserting these pairs into our trie, our searches are straightforward. + +### Code in Different Languages + + + + + +```cpp +#include + +class TrieNode { +public: + std::unordered_map children; + int weight; + TrieNode() { + weight = 0; + } +}; + +class WordFilter { +private: + TrieNode trie; + +public: + WordFilter(std::vector& words) { + int wt = 0; + for (const std::string& word : words) { + TrieNode* cur = ≜ + cur->weight = wt; + int L = word.length(); + for (int i = 0; i < L; ++i) { + TrieNode* tmp = cur; + for (int j = i; j < L; ++j) { + int code = (word[j] - '`') * 27; + if (tmp->children.count(code) == 0) { + tmp->children[code] = new TrieNode(); + } + tmp = tmp->children[code]; + tmp->weight = wt; + } + + tmp = cur; + for (int k = L - 1 - i; k >= 0; --k) { + int code = (word[k] - '`'); + if (tmp->children.count(code) == 0) { + tmp->children[code] = new TrieNode(); + } + tmp = tmp->children[code]; + tmp->weight = wt; + } + + int code = (word[i] - '`') * 27 + (word[L - 1 - i] - '`'); + if (cur->children.count(code) == 0) { + cur->children[code] = new TrieNode(); + } + cur = cur->children[code]; + cur->weight = wt; + } + wt++; + } + } + + int f(std::string prefix, std::string suffix) { + TrieNode* cur = ≜ + int i = 0, j = suffix.length() - 1; + while (i < prefix.length() || j >= 0) { + char c1 = i < prefix.length() ? prefix[i] : '`'; + char c2 = j >= 0 ? suffix[j] : '`'; + int code = (c1 - '`') * 27 + (c2 - '`'); + if (cur->children.count(code) == 0) { + return -1; + } + cur = cur->children[code]; + i++; + j--; + } + return cur->weight; + } +}; + + +``` + + + + +```java +class WordFilter { + TrieNode trie; + public WordFilter(String[] words) { + trie = new TrieNode(); + int wt = 0; + for (String word: words) { + TrieNode cur = trie; + cur.weight = wt; + int L = word.length(); + char[] chars = word.toCharArray(); + for (int i = 0; i < L; ++i) { + + TrieNode tmp = cur; + for (int j = i; j < L; ++j) { + int code = (chars[j] - '`') * 27; + if (tmp.children.get(code) == null) { + tmp.children.put(code, new TrieNode()); + } + tmp = tmp.children.get(code); + tmp.weight = wt; + } + + tmp = cur; + for (int k = L - 1 - i; k >= 0; --k) { + int code = (chars[k] - '`'); + if (tmp.children.get(code) == null) { + tmp.children.put(code, new TrieNode()); + } + tmp = tmp.children.get(code); + tmp.weight = wt; + } + + int code = (chars[i] - '`') * 27 + (chars[L - 1 - i] - '`'); + if (cur.children.get(code) == null) { + cur.children.put(code, new TrieNode()); + } + cur = cur.children.get(code); + cur.weight = wt; + + } + wt++; + } + } + + public int f(String prefix, String suffix) { + TrieNode cur = trie; + int i = 0, j = suffix.length() - 1; + while (i < prefix.length() || j >= 0) { + char c1 = i < prefix.length() ? prefix.charAt(i) : '`'; + char c2 = j >= 0 ? suffix.charAt(j) : '`'; + int code = (c1 - '`') * 27 + (c2 - '`'); + cur = cur.children.get(code); + if (cur == null) { + return -1; + } + i++; + j--; + } + return cur.weight; + } +} + +class TrieNode { + Map children; + int weight; + public TrieNode() { + children = new HashMap(); + weight = 0; + } +} +``` + + + + + +```python +Trie = lambda: collections.defaultdict(Trie) +WEIGHT = False + +class WordFilter: + def __init__(self, words: List[str]): + self.trie = Trie() + + for weight, word in enumerate(words): + cur = self.trie + cur[WEIGHT] = weight + for i, x in enumerate(word): + #Put all prefixes and suffixes + tmp = cur + for letter in word[i:]: + tmp = tmp[letter, None] + tmp[WEIGHT] = weight + + tmp = cur + for letter in word[:-i or None][::-1]: + tmp = tmp[None, letter] + tmp[WEIGHT] = weight + + #Advance letters + cur = cur[x, word[~i]] + cur[WEIGHT] = weight + + def f(self, pref: str, suff: str) -> int: + cur = self.trie + for a, b in zip_longest(pref, suff[::-1]): + if (a, b) not in cur: + return -1 + cur = cur[a, b] + return cur[WEIGHT] +``` + + + +### Complexity Analysis + +#### Time Complexity: $O(NK^2+QK)$ + +> **Reason**: where `N` is the number of words, `K` is the maximum length of a word, and `Q` is the number of queries. + + +#### Space Complexity: $O(NK^2)$ + +> **Reason**: the size of the trie. + +## Approach 3: Trie of Suffix Wrapped Words +### Intuition and Algorithm + +Intuition and Algorithm + +Consider the word `'apple'`. For each suffix of the word, we could insert that suffix, followed by `'#'`, followed by the word, all into the trie. + +For example, we will insert `'#apple', 'e#apple', 'le#apple', 'ple#apple', 'pple#apple', 'apple#apple'` into the trie. Then for a query like `prefix = "ap", suffix = "le"`, we can find it by querying our trie for `le#ap`. + +### Code in Different Languages + + + + + +```cpp +class TrieNode { +public: + TrieNode* children[27]; + int weight; + TrieNode() { + memset(children, 0, sizeof(children)); + weight = 0; + } +}; + +class WordFilter { +public: + TrieNode trie; + WordFilter(vector& words) { + for (int weight = 0; weight < words.size(); ++weight) { + string word = words[weight] + "{"; + for (int i = 0; i < word.length(); ++i) { + TrieNode* cur = ≜ + cur->weight = weight; + for (int j = i; j < 2 * word.length() - 1; ++j) { + int k = word[j % word.length()] - 'a'; + if (!cur->children[k]) { + cur->children[k] = new TrieNode(); + } + cur = cur->children[k]; + cur->weight = weight; + } + } + } + } + + int f(string prefix, string suffix) { + TrieNode* cur = ≜ + for (char letter : (suffix + '{' + prefix)) { + if (!cur->children[letter - 'a']) { + return -1; + } + cur = cur->children[letter - 'a']; + } + return cur->weight; + } +}; + + + + +``` + + + + +```java +class WordFilter { + TrieNode trie; + public WordFilter(String[] words) { + trie = new TrieNode(); + for (int weight = 0; weight < words.length; ++weight) { + String word = words[weight] + "{"; + for (int i = 0; i < word.length(); ++i) { + TrieNode cur = trie; + cur.weight = weight; + for (int j = i; j < 2 * word.length() - 1; ++j) { + int k = word.charAt(j % word.length()) - 'a'; + if (cur.children[k] == null) { + cur.children[k] = new TrieNode(); + } + cur = cur.children[k]; + cur.weight = weight; + } + } + } + } + public int f(String prefix, String suffix) { + TrieNode cur = trie; + for (char letter: (suffix + '{' + prefix).toCharArray()) { + if (cur.children[letter - 'a'] == null) { + return -1; + } + cur = cur.children[letter - 'a']; + } + return cur.weight; + } +} + +class TrieNode { + TrieNode[] children; + int weight; + public TrieNode() { + children = new TrieNode[27]; + weight = 0; + } +} +``` + + + + + +```python +Trie = lambda: collections.defaultdict(Trie) +WEIGHT = False + +class WordFilter: + def __init__(self, words: List[str]): + self.trie = Trie() + + for weight, word in enumerate(words): + word += '#' + for i in range(len(word)): + cur = self.trie + cur[WEIGHT] = weight + for j in range(i, 2 * len(word) - 1): + cur = cur[word[j % len(word)]] + cur[WEIGHT] = weight + + def f(self, pref: str, suff: str) -> int: + cur = self.trie + for letter in suff + '#' + pref: + if letter not in cur: + return -1 + cur = cur[letter] + return cur[WEIGHT] +``` + + + +### Complexity Analysis + +#### Time Complexity: $O(NK^2+QK)$ + +> **Reason**: `nums` is divided into half each time. In the worst-case scenario, we need to cut `nums` until the range has no element, it takes logarithmic time to reach this break condition. + +#### Space Complexity: $O(NK^2)$ + +> **Reason**: the size of the trie. + +## Video Solution + + + +## References + +- **LeetCode Problem**: [Prefix and Suffix Search](https://leetcode.com/problems/prefix-and-suffix-search/description/) + +- **Solution Link**: [Prefix and Suffix Search](https://leetcode.com/problems/prefix-and-suffix-search/solutions/)