Given a dictionary of roots, replace every word in a sentence with the shortest root that is a prefix of it. Build a trie of the roots, then walk each word char by char — the first node flagged isEnd is the winning root.
You're given a dictionary of roots and a sentence of space-separated words. If a word has a root in the dictionary as a prefix, replace the word with that root; if several roots match, use the shortest. Words with no matching root stay unchanged. Example: dict=["cat","bat","rat"], sentence="the cattle was rattled by the battery" → "the cat was rat by the bat".
isEnd. Now walk a word character by character down the trie: the first time you step onto an isEnd node, the characters consumed so far ARE the shortest matching root — stop immediately. If you fall off the trie (a child is missing) before hitting any isEnd, no root matches and the word is kept as-is.isEnd = true on the last node — that flag is what marks a complete root.isEnd node, return the accumulated prefix — because you walked left-to-right, this is the shortest root that prefixes the word.isEnd, the word has no root in the dictionary, so emit the original word unchanged.join(' ') the result back into a sentence.isEnd as soon as you arrive on a node, not only after consuming the whole word. For "cattle" you stop at "cat" the instant its node is flagged; if you kept walking to the end of "cattle"you'd fall off the trie and wrongly keep the original. First flag wins.Building the trie costs O(D) in the total characters of all roots. Then every word is resolved by a single walk of length O(min(word length, longest root)), so processing the whole sentence is O(S)in its total characters. The trie shares common prefixes among roots, and short-circuits at the first match — exactly the structure this "shortest matching prefix" query wants.
HashSet of roots can also work if you probe each prefix length of every word.["cat", "bat", "rat"], marking each root's last node isEnd.1▶class TrieNode {2▶ children: Record<string, TrieNode> = {};3▶ isEnd = false;4}56function replaceWords(dictionary: string[], sentence: string): string {7 // 1) Build a trie of the dictionary roots.8▶ const root = new TrieNode();9 for (const w of dictionary) {10 let node = root;11 for (const c of w) {12 if (!node.children[c]) node.children[c] = new TrieNode();13 node = node.children[c];14 }15 node.isEnd = true; // mark end-of-root16 }1718 // 2) For each word, walk the trie until the first end-of-root.19 const shortestRoot = (word: string): string => {20 let node = root;21 let prefix = '';22 for (const c of word) {23 if (!node.children[c]) break; // fell off the trie → no root24 prefix += c;25 node = node.children[c];26 if (node.isEnd) return prefix; // first root wins (shortest)27 }28 return word; // no root matched → keep original29 };3031 return sentence.split(' ').map(shortestRoot).join(' ');32}
class TrieNode {
children: Record<string, TrieNode> = {};
isEnd = false;
}
function replaceWords(dictionary: string[], sentence: string): string {
// 1) Build a trie of the dictionary roots.
const root = new TrieNode();
for (const w of dictionary) {
let node = root;
for (const c of w) {
if (!node.children[c]) node.children[c] = new TrieNode();
node = node.children[c];
}
node.isEnd = true; // mark end-of-root
}
// 2) For each word, walk the trie until the first end-of-root.
const shortestRoot = (word: string): string => {
let node = root;
let prefix = '';
for (const c of word) {
if (!node.children[c]) break; // fell off the trie → no root
prefix += c;
node = node.children[c];
if (node.isEnd) return prefix; // first root wins (shortest)
}
return word; // no root matched → keep original
};
return sentence.split(' ').map(shortestRoot).join(' ');
}TrieNode is a children map (char → node) plus an isEnd flag. The character is the edge (the map key), and isEnd marks the last node of a complete dictionary root.isEnd = true on the terminal node. Roots that share a prefix (none here, but e.g. "a" and "ab") share nodes.prefix. For each character of the word, if the child is missing break (no root prefixes this word); otherwise append the char and descend.node.isEnd. Because we go left-to-right, the first isEnd we hit is the shortest root, so we return prefix immediately. If we never hit one, return the original word.shortestRoot, and join(' ') back into the final sentence.O(D) where D is the total characters across all roots. Each word is one walk of length O(min(word, longest root)), so the whole sentence is O(S) in its characters. Space is O(D) for the trie.isEnd guarantees that for free.TrieNode[26] indexed by c - 'a'.Set and, for each word, test every prefix length word.slice(0, k) shortest-first — simpler but O(word²) per word.isEnd on the last char and return it — the word is replaced by itself, which is correct.class TrieNode {
children: Record<string, TrieNode> = {};
isEnd = false;
}
function replaceWords(dictionary: string[], sentence: string): string {
// 1) Build a trie of the dictionary roots.
const root = new TrieNode();
for (const w of dictionary) {
let node = root;
for (const c of w) {
if (!node.children[c]) node.children[c] = new TrieNode();
node = node.children[c];
}
node.isEnd = true; // mark end-of-root
}
// 2) For each word, walk the trie until the first end-of-root.
const shortestRoot = (word: string): string => {
let node = root;
let prefix = '';
for (const c of word) {
if (!node.children[c]) break; // fell off the trie → no root
prefix += c;
node = node.children[c];
if (node.isEnd) return prefix; // first root wins (shortest)
}
return word; // no root matched → keep original
};
return sentence.split(' ').map(shortestRoot).join(' ');
}O(D) where D is the total characters across all roots. Each word is one walk of length O(min(word, longest root)), so the whole sentence is O(S) in its characters. Space is O(D) for the trie.Skip the trie: store the roots in a Set, and for each word test its prefixes shortest-first, returning the first one that is a known root.
function replaceWords(dictionary: string[], sentence: string): string {
const roots = new Set(dictionary);
const replace = (word: string): string => {
for (let k = 1; k <= word.length; k++) {
const pre = word.slice(0, k);
if (roots.has(pre)) return pre; // shortest prefix that is a root
}
return word;
};
return sentence.split(' ').map(replace).join(' ');
}L prefixes (sliceis O(k)), so it's O(word²) per word. The trie walks each character once and short-circuits at the first isEnd.| replace word with its dictionary root / prefix | trie of roots + walk |
| shortest matching prefix wins | return on FIRST isEnd while walking |
| no root matches | child missing → keep original word |
| process every word of a sentence | split → map → join(" ") |
class TrieNode { children: Record<string, TrieNode> = {}; isEnd = false; }
function replaceWords(dict: string[], sentence: string): string {
const root = new TrieNode();
for (const w of dict) {
let n = root;
for (const c of w) n = (n.children[c] ??= new TrieNode());
n.isEnd = true;
}
const root1 = (word: string): string => {
let n = root, pre = '';
for (const c of word) {
if (!n.children[c]) break;
pre += c; n = n.children[c];
if (n.isEnd) return pre;
}
return word;
};
return sentence.split(' ').map(root1).join(' ');
}trie of the roots with isEndmarking each root's terminal node. One walk per word answers it.