1. Problem Statement
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 thewordsin the dictionary.int f(String pref, String suff)Returns the index of the word in the dictionary, which has the prefixprefand the suffixsuff. If there is more than one valid index, return the largest of them. If there is no such word in the dictionary, return-1.
2. Approach: The "Wrapped Word" Trie
Searching by prefix is easy with a Trie. Searching by suffix requires a reverse Trie. But searching by BOTH simultaneously is hard.
The "Aha!" Moment
We can combine the suffix and prefix into a single string and insert it into a standard Trie!
For a word like "apple" with index 0, we insert the following variations into the Trie:
"apple{apple""pple{apple""ple{apple""le{apple""e{apple""{apple"
If the user queries pref = "ap" and suff = "le", we simply search our Trie for the combined string: "le{ap".
- State: Every TrieNode stores the
weight(the index of the word). Since we process words in order, overwriting theweightautomatically guarantees we store the largest index. - Separator: We use
{because its ASCII value is immediately afterz, making the Trie array size 27.
3. Java Implementation
class WordFilter {
class TrieNode {
TrieNode[] children = new TrieNode[27];
int weight = -1;
}
TrieNode root;
public WordFilter(String[] words) {
root = new TrieNode();
for (int weight = 0; weight < words.length; weight++) {
String word = words[weight] + "{" + words[weight];
for (int i = 0; i <= words[weight].length(); i++) {
TrieNode curr = root;
curr.weight = weight;
// Insert the suffix{prefix substring into the Trie
for (int j = i; j < word.length(); j++) {
int k = word.charAt(j) - 'a';
if (curr.children[k] == null) {
curr.children[k] = new TrieNode();
}
curr = curr.children[k];
curr.weight = weight;
}
}
}
}
public int f(String pref, String suff) {
TrieNode curr = root;
String query = suff + "{" + pref;
for (char c : query.toCharArray()) {
if (curr.children[c - 'a'] == null) {
return -1;
}
curr = curr.children[c - 'a'];
}
return curr.weight;
}
}
4. 5-Minute "Video-Style" Walkthrough
- The Space-Time Tradeoff: This approach generates $L$ suffixes for each word of length $L$. Therefore, we insert $O(L)$ strings into the Trie per word. We are trading memory (Space) for blazing fast $O(Prefix + Suffix)$ lookup times.
- The Overwrite Logic: The problem asks for the largest index if there are duplicates. Because our loop
weight = 0towords.lengthgoes in ascending order, we blindly updatecurr.weight = weightat every node. The last word to pass through a node leaves its highest index there permanently.
5. Interview Discussion
- Interviewer: "What is the time complexity?"
- You: "Initialization takes $O(N \times L^2)$ where $N$ is the number of words and $L$ is the max word length. The search
f()takes strictly $O(P + S)$ where $P$ is prefix length and $S$ is suffix length." - Interviewer: "What if memory is heavily constrained?"
- You: "We could maintain two separate Tries (one forward, one backward). During
f(), we get a list of valid indices from the prefix Trie, and a list of valid indices from the suffix Trie. Then we find the intersection. It saves memory but makesf()much slower."