On this page
Description
Some questions care about every prefix of a word, not just the word: "the longest word that can be built one letter at a time from other words in the list", or "replace each word in a sentence with the shortest dictionary root it starts with". A trie holds all the prefixes on one path, so each question becomes a single walk.
This skill covers three versions of that walk: requiring an end mark on every node, stopping at the first end mark, and keeping the last one. It also covers the tie-break and "no answer" rules that make the results well defined.
We'll go from simple to more technical:
- The pattern: the longest word whose every prefix is also a word.
- A second flavor: Replace Words, stopping at the first end mark.
- Where prefix walks show up (stemming, URL routing, IP longest-prefix match).
- What breaks it: checking only the full word, shortest versus longest, ties, no answer.
The approach
Java
boolean allPrefixesAreWords(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
node = node.next[c - 'a'];
if (node == null || !node.isEnd) return false; // this prefix isn't itself a stored word
}
return true;
}
String longestWordWithAllPrefixes(String[] words) {
for (String w : words) insert(w);
String best = "";
for (String w : words) {
if (allPrefixesAreWords(w) && (w.length() > best.length()
|| (w.length() == best.length() && w.compareTo(best) < 0))) {
best = w; // longer, or same length and smaller
}
}
return best;
}
Python
def all_prefixes_are_words(word: str) -> bool:
node = root
for c in word:
node = node.next.get(c)
if node is None or not node.is_end:
return False # this prefix isn't itself a stored word
return True
def longest_word_with_all_prefixes(words: list[str]) -> str:
for w in words:
insert(w)
best = ""
for w in words:
if all_prefixes_are_words(w) and (len(w) > len(best) or (len(w) == len(best) and w < best)):
best = w # longer, or same length and smaller
return best
"bread" qualifies only if "b", "br", "bre", "brea" and "bread" are all in the list - every node on its path must carry an end mark, not just the last. A plain search(w) checks only the last one. The tie-break (lexicographically smallest among equally long words) makes the answer unique, and "" is the answer when no word qualifies, for instance when there are no one-letter words.
A second flavor: stop at the first end mark
Replace each word by the shortest root that is a prefix of it ("cattle" with root "cat" becomes "cat"):
Java
String shortestRoot(String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
node = node.next[word.charAt(i) - 'a'];
if (node == null) return word; // no root is a prefix
if (node.isEnd) return word.substring(0, i + 1); // the first end mark is the shortest root
}
return word;
}
Python
def shortest_root(word: str) -> str:
node = root
for i, c in enumerate(word):
node = node.next.get(c)
if node is None:
return word # no root is a prefix
if node.is_end:
return word[:i + 1] # the first end mark is the shortest root
return word
Walking down the word's own path meets the roots in order of length, so the first end mark is the shortest. Continuing to the last end mark returns the longest root instead ("cat" and "catt" for "cattle" would give "catt").
Where you'll see it
Stemming and lemmatization shorten words to dictionary roots; URL routers match the most (or least) specific registered path prefix; IP forwarding picks the longest matching network prefix - the same walk, keeping the last end mark seen. Checking that every intermediate step is itself valid (every prefix a word) is how incremental builds validate each step of a chain.
Which tool for which job?
| You need | Use |
|---|---|
| Every prefix of a word is itself a word | one walk, every node end-marked |
| The longest such word | a DFS over end-marked children only |
| The shortest dictionary root of a word | stop at the first end mark |
| The most specific matching rule (routes, IPs) | keep the last end mark seen |
| Every pattern occurring anywhere in a text | Aho-Corasick |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| Checking only the full word | "bread" passes without "brea" | every node on the path needs its end mark |
| Keeping the last end mark for the shortest root | "cattle" becomes "catt" | stop at the first end mark |
| No tie-break | equally long answers come out in input order | longest, then lexicographically smallest |
| No "no answer" case | a word with no root is dropped | return the word itself (or "") |
| A set of prefix strings | O(L^2) characters built per word | walk the trie once |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. Why isn't search(word) enough for "every prefix is also a word"?
Show answer
search only checks the end mark at the last node. "bread" can be in the list while "brea" isn't; every node on the path must be marked.
2. How does a single walk find the shortest root of a word?
Show answer
Walking the word's path meets roots in increasing length, so the first node with an end mark is the shortest root. Stop there.
3. What does the longest-word question return when no word qualifies?
Show answer
An empty string - for example when the list has no one-letter words, since every qualifying word must start with one.
Key takeaways
- A word's prefixes all lie on its path; one walk checks them.
- "Every prefix is a word": every node on the path needs an end mark.
- Shortest root: stop at the first end mark; longest: keep the last.
- Make tie-breaks and "no answer" explicit.