Replace Words

Medium Tries

Problem

Given a dictionary of words and a sentence, replace all successors in the sentence with the shortest root in the dictionary that is a prefix of the word, and return the modified sentence.

  • 1 ≤ dictionary.length ≤ 10⁵
  • 1 ≤ dictionary[i].length ≤ 100
  • 1 ≤ sentence.length ≤ 10⁶
  • dictionary[i] and sentence consist only of lowercase letters
  • The sentence consists of words separated by single spaces

Example

Input: dictionary = ["cat", "bat", "rat"], sentence = "the cattle was rattled by the battery"
Output: "the cat was rat by the bat"

The brute-force approach would check each word in the sentence against every root in the dictionary to find the shortest prefix, resulting in a costly O(m * n * k) time complexity where m is the number of words in the sentence, n is the number of roots, and k is the average root length. The optimal approach uses a Trie to efficiently find the shortest prefix for each word. For example, for the word 'cattle', the Trie traversal finds 'cat' as the shortest prefix root, replacing 'cattle' with 'cat'. Similarly, 'rattled' is replaced by 'rat' and 'battery' by 'bat'. This reduces the prefix search to O(k) per word, where k is the length of the word.

Approach

Straightforward Solution

A naive solution checks each word in the sentence against every root in the dictionary to find the shortest prefix. This leads to O(m * n * k) time complexity, which is impractical for large inputs.

Core Observation

The problem reduces to efficiently finding the shortest prefix of each word in the sentence that exists in the dictionary. This is a classic prefix search problem where a Trie data structure excels by enabling prefix queries in O(k) time, where k is the length of the word.

Path to Optimal

Recognizing the prefix search nature, the dictionary is stored in a Trie, where each node represents a character and may mark the end of a root word. For each word in the sentence, the algorithm traverses the Trie character by character until it finds a node marking a root or exhausts the path. This traversal finds the shortest prefix root in O(k) time per word, drastically improving efficiency.

Optimal Approach

Build a Trie from the dictionary roots. For each word in the sentence, traverse the Trie to find the shortest root prefix. If found, replace the word with this root; otherwise, keep the original word. Join all processed words to form the final sentence. This approach achieves O(N + M * K) time complexity, where N is total characters in dictionary, M is number of words in sentence, and K is average word length.

Time

O(N + M * K)

Building the Trie takes O(N) time, where N is the sum of lengths of all dictionary words. Searching for prefixes for each of the M words in the sentence takes O(K) time per word, where K is the average length of the words, resulting in O(M * K).

Space

O(N)

The Trie stores all dictionary words character by character, requiring O(N) auxiliary space proportional to the total length of all dictionary words. The output string space is excluded.

Pattern Spotlight

Trie (Prefix Tree for Efficient Prefix Matching)

When needing to find the shortest prefix from a large dictionary for multiple query words, build a Trie to represent all prefixes, then traverse it per query word to find the earliest prefix match, enabling O(k) prefix search instead of costly repeated string comparisons.

Solution

1class TrieNode:
2 def __init__(self):
3 self.children = {}
4 self.word = None
5
6class Solution:
7 def replaceWords(self, dictionary: List[str], sentence: str) -> str:
8 root = TrieNode()
9
10 for word in dictionary:
11 node = root
12 for c in word:
13 if c not in node.children:
14 node.children[c] = TrieNode()
15 node = node.children[c]
16 node.word = word
17
18 res = []
19
20 for word in sentence.split():
21 node = root
22 replacement = word
23
24 for c in word:
25 if c not in node.children:
26 break
27 node = node.children[c]
28 if node.word:
29 replacement = node.word
30 break
31
32 res.append(replacement)
33
34 return " ".join(res)

Step-by-Step Solution

1

Construct Trie from Dictionary Roots

8root = TrieNode()
10for word in dictionary:
11 node = root
12 for c in word:
13 if c not in node.children:
14 node.children[c] = TrieNode()
15 node = node.children[c]
16 node.word = word

Objective

To build a prefix tree that compactly represents all dictionary roots for efficient prefix lookup.

Key Insight

Building a Trie allows shared prefixes among dictionary roots to be stored once, reducing redundant comparisons. Each node holds children nodes keyed by characters and optionally stores a root word when a root ends at that node. This structure enables fast prefix queries by traversing character by character.

Interview Quick-Check

Core Logic

Insert each root word into the Trie by creating or traversing child nodes for each character, marking the end node with the root word.

Common Pitfalls & Bugs

Forgetting to mark the end of a root word in the Trie node leads to incorrect prefix matches.

2

Traverse Trie to Replace Words with Shortest Prefix

18res = []
20for word in sentence.split():
21 node = root
22 replacement = word
24 for c in word:
25 if c not in node.children:
26 break
27 node = node.children[c]
28 if node.word:
29 replacement = node.word
30 break
32 res.append(replacement)

Objective

To find and replace each word in the sentence with its shortest root prefix from the Trie, if such a prefix exists.

Key Insight

For each word, traversing the Trie character by character allows early termination as soon as a root prefix is found, ensuring the shortest prefix is selected. If no prefix is found, the original word remains. This avoids costly string comparisons and leverages the Trie structure for efficient prefix detection.

Interview Quick-Check

Core Logic

Traverse the Trie for each character in the word; if a node with a root word is found, replace the word immediately with that root.

State & Boundaries

Break traversal if a character is missing in the Trie children, indicating no prefix match.

Common Pitfalls & Bugs

Not breaking early upon finding a root word leads to longer prefixes being chosen instead of the shortest.

3

Join Replaced Words to Form Final Sentence

34return " ".join(res)

Objective

To combine all processed words into a single string representing the modified sentence.

Key Insight

After replacing words with their shortest prefix roots where applicable, joining the list of words with spaces reconstructs the sentence in the required format.

Interview Quick-Check

Core Logic

Use a single join operation to efficiently concatenate all words with spaces.

Line Analysis

This solution has 5 Critical lines interviewers watch for.

Line 4 Critical
self.word = None

Initialize the word attribute to None to mark the end of a root word.

Storing the root word at the terminal node allows quick identification of prefix matches during traversal.

Line 16 Critical
node.word = word

Mark the end of the root word by storing it in the node.

This marks the node as a valid root prefix, enabling early prefix detection during search.

Line 28 Critical
if node.word:

Check if the current node marks the end of a root word.

Finding a node with a stored root word indicates a valid prefix match has been found.

Line 29 Critical
replacement = node.word

Update the replacement word to the found root prefix.

Replacing the original word with the shortest prefix root satisfies the problem requirement.

Line 30 Critical
break

Break out of the loop after finding the shortest prefix.

Stopping traversal ensures the shortest prefix is used, preventing longer prefixes from overriding it.

Line 3 Core
self.children = {}

Initialize the children dictionary for the TrieNode.

This dictionary stores child nodes keyed by characters, enabling efficient traversal and insertion of prefixes.

Line 14 Core
node.children[c] = TrieNode()

Create a new TrieNode for the missing character.

This extends the Trie to include the new prefix path for the root word.

Line 25 Core
if c not in node.children:

Check if the current character is not a child of the current node.

If the character is missing, no prefix match exists beyond this point, so traversal stops.

Line 26 Core
break

Break out of the loop if no child node exists for the character.

Early termination prevents unnecessary traversal when no prefix match is possible.

Line 8 Core
root = TrieNode()

Create the root node of the Trie.

The root node serves as the entry point for all prefix insertions and lookups, representing an empty prefix.

Line 13 Core
if c not in node.children:

Check if the current character is not already a child of the current node.

If the character is missing, a new node must be created to represent this prefix path.

Line 15 Core
node = node.children[c]

Move to the child node corresponding to the current character.

Advancing the node pointer allows insertion or traversal to continue along the prefix path.

Line 21 Core
node = root

Start traversal from the root node for the current word.

Prefix search begins at the root of the Trie for each word to find the shortest matching root.

Line 24 Core
for c in word:

Iterate over each character in the current word.

Traverse the Trie character by character to find the shortest prefix root.

Line 27 Core
node = node.children[c]

Move to the child node corresponding to the current character.

Advancing the node pointer continues the prefix search along the Trie path.

Line 10 Standard
for word in dictionary:

Iterate over each root word in the dictionary.

Each root word must be inserted into the Trie to build the prefix structure for efficient lookup.

Line 11 Standard
node = root

Start traversal from the root node for the current root word.

Insertion begins at the root, moving down or creating child nodes for each character.

Line 12 Standard
for c in word:

Iterate over each character in the root word.

Each character corresponds to a level in the Trie, guiding node creation or traversal.

Line 20 Standard
for word in sentence.split():

Split the sentence into individual words and iterate over them.

Processing each word separately allows prefix replacement on a per-word basis.

Line 22 Standard
replacement = word

Initialize replacement to the original word.

If no prefix is found, the original word remains unchanged in the result.

Line 32 Standard
res.append(replacement)

Append the replacement word to the result list.

Collecting the processed words allows reconstruction of the final sentence after all replacements.

Line 18 Standard
res = []

Initialize the result list to store processed words.

This list accumulates the final words after prefix replacement for efficient joining later.

Line 34 Standard
return " ".join(res)

Join all words in the result list with spaces to form the final sentence.

Joining the list into a string produces the output in the required format efficiently.

Test Your Understanding

Why does using a Trie improve prefix search efficiency compared to checking each root against every word?

A Trie organizes all dictionary roots into a tree structure where common prefixes share nodes, allowing prefix searches to be done in O(k) time per word by traversing the tree once, instead of repeatedly comparing each root string, which would be O(n*k) per word.

Related Problems

Tries pattern

Don't just read it. Drill it.

Reconstruct Replace Words from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Replace Words drill

or drill a free problem