1858. Longest Word With All Prefixes ๐
Problem Description
You are given an array of strings called words
. Your task is to find the longest string in this array that has a special property: every prefix of this string must also exist in the words
array.
A prefix of a string is any substring that starts from the beginning. For example, if we have the string "app"
, its prefixes are:
"a"
(prefix of length 1)"ap"
(prefix of length 2)"app"
(the entire string itself)
For the string to be valid, all of these prefixes must be present in the words
array.
Example:
If words = ["a", "app", "ap"]
, the string "app"
is valid because:
"a"
is inwords
โ"ap"
is inwords
โ"app"
is inwords
โ
Important Rules:
- If multiple strings have the same maximum length and satisfy the prefix condition, return the lexicographically smallest one (the one that would come first in dictionary order)
- If no string satisfies the condition, return an empty string
""
The solution uses a Trie data structure to efficiently check if all prefixes of a word exist in the array. The Trie allows us to traverse each word character by character and verify at each step whether that prefix was inserted as a complete word (marked by is_end = True
). The algorithm inserts all words into the Trie, then checks each word to see if all its prefixes are valid words, keeping track of the longest valid word that is lexicographically smallest when there are ties.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: While this problem doesn't explicitly mention a graph, we can model it as a graph problem. The Trie data structure used in the solution is essentially a tree (which is a special type of graph). Each word can be thought of as a path in the Trie, and we need to traverse these paths to verify that all prefixes exist.
Is it a tree?
- Yes: The Trie is a tree structure where each node represents a character, and paths from root to leaf nodes represent words. We're traversing this tree structure to check if all prefixes of a word exist as complete words in the tree.
DFS
- Yes: The solution uses DFS implicitly through the Trie traversal. When we search for a word in the Trie, we perform a depth-first traversal from the root node, going deeper character by character. At each level, we check if that prefix is marked as a complete word (
is_end = True
). This is essentially a DFS where we explore one path completely before backtracking.
Conclusion: The flowchart correctly leads us to use DFS for this problem. The Trie-based solution implements DFS by:
- Building a tree structure (Trie) from all words
- For each word, performing a depth-first traversal to verify that every prefix along the path is a valid word
- The search operation traverses from root to leaf, checking the
is_end
flag at each node to ensure all prefixes exist as complete words
Intuition
The key insight is recognizing that we need to efficiently check if all prefixes of a word exist in our word list. If we use a naive approach of checking each prefix against the entire words
array, we'd have inefficient repeated lookups.
Think about how prefixes work - they share common characters from the beginning. For example, "a"
, "ap"
, and "app"
all start with "a"
. This naturally suggests a tree-like structure where common prefixes share the same path from the root.
A Trie (prefix tree) is perfect for this because:
- Shared prefix paths: Words with common prefixes share nodes in the tree, avoiding redundant storage
- Efficient prefix checking: As we traverse down the Trie following a word's characters, we can check at each node whether that prefix forms a complete word
The clever part is marking nodes with an is_end
flag. When we insert "a"
, we mark that node as an endpoint. When we later insert "app"
, we traverse through the "a"
node (already marked as an endpoint) and continue to "p"
and another "p"
, marking the final node.
During the search phase, we can verify the "all prefixes exist" requirement by checking if every node along our path has is_end = True
. If we encounter any node where is_end = False
, it means that prefix doesn't exist as a standalone word in our original array, so we can immediately reject that candidate.
This transforms our problem from "check if all prefixes exist" to "traverse a path in the Trie and verify all nodes are marked as word endings" - a much more elegant and efficient solution.
Learn more about Depth-First Search and Trie patterns.
Solution Approach
The solution implements a Trie data structure with two main operations: insert
and search
.
Trie Node Structure: Each Trie node contains:
children
: An array of size 26 (for lowercase letters a-z), where each position corresponds to a letteris_end
: A boolean flag indicating whether this node marks the end of a valid word
Building the Trie (insert
operation):
For each word in words
:
- Start from the root node
- For each character
c
in the word:- Calculate its index:
idx = ord(c) - ord("a")
(converts 'a' to 0, 'b' to 1, etc.) - If no child exists at
children[idx]
, create a new Trie node - Move to that child node
- Calculate its index:
- After processing all characters, mark
is_end = True
for the final node
Validating Words (search
operation):
For each word, verify that all its prefixes exist:
- Start from the root node
- For each character
c
in the word:- Calculate its index:
idx = ord(c) - ord("a")
- Move to the child node at
children[idx]
- Check if
is_end
isTrue
- if not, this prefix doesn't exist as a complete word, returnFalse
- Calculate its index:
- If we successfully traverse the entire word with all prefixes valid, return
True
Finding the Answer:
- Insert all words into the Trie
- For each word
w
inwords
:- Check if it's valid using
trie.search(w)
- If valid and either:
- It's longer than the current answer, OR
- It has the same length but is lexicographically smaller (
w < ans
)
- Update the answer to
w
- Check if it's valid using
- Return the final answer
The time complexity is O(n ร m)
where n
is the number of words and m
is the average word length, for both building the Trie and searching. The space complexity is O(total_characters)
for storing the Trie.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with words = ["w", "wo", "wor", "worl", "world"]
.
Step 1: Build the Trie
Starting with an empty Trie (root node), we insert each word:
Inserting "w":
- From root, create child at index 22 (w = 22nd letter)
- Mark this node with
is_end = True
Inserting "wo":
- From root, go to existing child at index 22 (w)
- Create new child at index 14 (o = 15th letter)
- Mark this 'o' node with
is_end = True
Inserting "wor":
- From root, traverse: w โ o (both exist)
- Create new child at index 17 (r = 18th letter)
- Mark this 'r' node with
is_end = True
Inserting "worl":
- From root, traverse: w โ o โ r (all exist)
- Create new child at index 11 (l = 12th letter)
- Mark this 'l' node with
is_end = True
Inserting "world":
- From root, traverse: w โ o โ r โ l (all exist)
- Create new child at index 3 (d = 4th letter)
- Mark this 'd' node with
is_end = True
Our Trie now looks like:
root | w (is_end=True) | o (is_end=True) | r (is_end=True) | l (is_end=True) | d (is_end=True)
Step 2: Search for Valid Words
Initialize ans = ""
Checking "w":
- Traverse from root to 'w'
- At 'w':
is_end = True
โ - Valid! Length = 1 > 0, so
ans = "w"
Checking "wo":
- Traverse: root โ w โ o
- At 'w':
is_end = True
โ - At 'o':
is_end = True
โ - Valid! Length = 2 > 1, so
ans = "wo"
Checking "wor":
- Traverse: root โ w โ o โ r
- At 'w':
is_end = True
โ - At 'o':
is_end = True
โ - At 'r':
is_end = True
โ - Valid! Length = 3 > 2, so
ans = "wor"
Checking "worl":
- Traverse: root โ w โ o โ r โ l
- At 'w':
is_end = True
โ - At 'o':
is_end = True
โ - At 'r':
is_end = True
โ - At 'l':
is_end = True
โ - Valid! Length = 4 > 3, so
ans = "worl"
Checking "world":
- Traverse: root โ w โ o โ r โ l โ d
- At 'w':
is_end = True
โ - At 'o':
is_end = True
โ - At 'r':
is_end = True
โ - At 'l':
is_end = True
โ - At 'd':
is_end = True
โ - Valid! Length = 5 > 4, so
ans = "world"
Result: Return "world"
as it's the longest word where all prefixes exist.
Counter-example: If we had words = ["w", "wor", "world"]
(missing "wo" and "worl"):
- "world" would fail the search at 'o' because
is_end = False
(prefix "wo" doesn't exist) - "wor" would fail at 'o' for the same reason
- Only "w" would be valid, so we'd return "w"
Solution Implementation
1from typing import List, Optional
2
3
4class Trie:
5 """A trie (prefix tree) data structure for efficient string operations."""
6
7 __slots__ = ["children", "is_end"]
8
9 def __init__(self):
10 """Initialize a trie node with 26 possible children (for lowercase letters)."""
11 self.children: List[Optional['Trie']] = [None] * 26
12 self.is_end: bool = False
13
14 def insert(self, word: str) -> None:
15 """
16 Insert a word into the trie.
17
18 Args:
19 word: The word to insert (assumed to contain only lowercase letters)
20 """
21 current_node = self
22
23 for char in word:
24 # Calculate the index for this character (0-25 for a-z)
25 index = ord(char) - ord('a')
26
27 # Create a new node if it doesn't exist
28 if current_node.children[index] is None:
29 current_node.children[index] = Trie()
30
31 # Move to the child node
32 current_node = current_node.children[index]
33
34 # Mark the end of the word
35 current_node.is_end = True
36
37 def search(self, word: str) -> bool:
38 """
39 Check if a word can be built character by character, where each prefix is also a word.
40
41 Args:
42 word: The word to search for
43
44 Returns:
45 True if every prefix of the word (including the word itself) exists as a complete word
46 """
47 current_node = self
48
49 for char in word:
50 # Calculate the index for this character
51 index = ord(char) - ord('a')
52
53 # Move to the child node
54 current_node = current_node.children[index]
55
56 # Check if this prefix is a complete word
57 if not current_node.is_end:
58 return False
59
60 return True
61
62
63class Solution:
64 def longestWord(self, words: List[str]) -> str:
65 """
66 Find the longest word that can be built one character at a time from other words.
67
68 Args:
69 words: List of words containing only lowercase letters
70
71 Returns:
72 The longest word that can be built character by character.
73 If multiple words have the same length, return the lexicographically smallest one.
74 """
75 # Build the trie with all words
76 trie = Trie()
77 for word in words:
78 trie.insert(word)
79
80 # Find the longest valid word
81 result = ""
82
83 for word in words:
84 # Check if this word is better than our current result
85 is_longer = len(word) > len(result)
86 is_same_length_but_smaller = len(word) == len(result) and word < result
87
88 # Update result if this word is valid and better
89 if (is_longer or is_same_length_but_smaller) and trie.search(word):
90 result = word
91
92 return result
93
1/**
2 * Trie (Prefix Tree) data structure for efficient string storage and retrieval
3 */
4class Trie {
5 // Array to store 26 child nodes (one for each lowercase letter)
6 private Trie[] children = new Trie[26];
7 // Flag to mark if current node represents end of a word
8 private boolean isEndOfWord;
9
10 /**
11 * Constructor to initialize the Trie
12 */
13 public Trie() {
14 }
15
16 /**
17 * Inserts a word into the trie
18 * @param word The word to be inserted
19 */
20 public void insert(String word) {
21 Trie currentNode = this;
22
23 // Traverse through each character in the word
24 for (char character : word.toCharArray()) {
25 // Calculate array index for the character (0-25 for a-z)
26 int index = character - 'a';
27
28 // Create new node if path doesn't exist
29 if (currentNode.children[index] == null) {
30 currentNode.children[index] = new Trie();
31 }
32
33 // Move to the child node
34 currentNode = currentNode.children[index];
35 }
36
37 // Mark the last node as end of word
38 currentNode.isEndOfWord = true;
39 }
40
41 /**
42 * Searches if a word can be built character by character
43 * Each prefix must exist as a complete word in the trie
44 * @param word The word to search for
45 * @return true if word can be built progressively, false otherwise
46 */
47 public boolean search(String word) {
48 Trie currentNode = this;
49
50 // Check each prefix of the word
51 for (char character : word.toCharArray()) {
52 // Calculate array index for the character
53 int index = character - 'a';
54
55 // Move to the child node
56 currentNode = currentNode.children[index];
57
58 // If current node is null or prefix is not a complete word, return false
59 if (currentNode == null || !currentNode.isEndOfWord) {
60 return false;
61 }
62 }
63
64 return true;
65 }
66}
67
68/**
69 * Solution class to find the longest word that can be built progressively
70 */
71class Solution {
72 /**
73 * Finds the longest word that can be built one character at a time
74 * If multiple words have same length, returns the lexicographically smallest
75 * @param words Array of words to process
76 * @return The longest word that can be built progressively
77 */
78 public String longestWord(String[] words) {
79 // Initialize trie and insert all words
80 Trie trie = new Trie();
81 for (String word : words) {
82 trie.insert(word);
83 }
84
85 // Track the best answer found so far
86 String longestValidWord = "";
87
88 // Check each word to see if it can be built progressively
89 for (String word : words) {
90 // Check if current word is better than existing answer
91 boolean isLonger = word.length() > longestValidWord.length();
92 boolean isSameLengthButSmaller = word.length() == longestValidWord.length()
93 && word.compareTo(longestValidWord) < 0;
94
95 // Update answer if word is valid and better than current answer
96 if ((isLonger || isSameLengthButSmaller) && trie.search(word)) {
97 longestValidWord = word;
98 }
99 }
100
101 return longestValidWord;
102 }
103}
104
1class Trie {
2private:
3 Trie* children[26]; // Array of pointers to child nodes (one for each letter a-z)
4 bool isEndOfWord; // Flag indicating if this node represents the end of a valid word
5
6public:
7 // Constructor: Initialize all children to nullptr and isEndOfWord to false
8 Trie() {
9 fill(begin(children), end(children), nullptr);
10 isEndOfWord = false;
11 }
12
13 // Insert a word into the trie
14 void insert(const string& word) {
15 Trie* currentNode = this;
16
17 // Traverse through each character in the word
18 for (char ch : word) {
19 int index = ch - 'a'; // Convert character to array index (0-25)
20
21 // Create new node if path doesn't exist
22 if (!currentNode->children[index]) {
23 currentNode->children[index] = new Trie();
24 }
25
26 // Move to the child node
27 currentNode = currentNode->children[index];
28 }
29
30 // Mark the end of the word
31 currentNode->isEndOfWord = true;
32 }
33
34 // Check if a word can be built character by character with all prefixes being valid words
35 bool search(const string& word) {
36 Trie* currentNode = this;
37
38 // Check each prefix of the word
39 for (char ch : word) {
40 int index = ch - 'a'; // Convert character to array index (0-25)
41
42 // Move to the child node
43 currentNode = currentNode->children[index];
44
45 // If current prefix is not a valid word, return false
46 if (!currentNode->isEndOfWord) {
47 return false;
48 }
49 }
50
51 // All prefixes are valid words
52 return true;
53 }
54};
55
56class Solution {
57public:
58 // Find the longest word that can be built one character at a time from other words
59 string longestWord(vector<string>& words) {
60 // Build trie with all words
61 Trie trie;
62 for (const string& word : words) {
63 trie.insert(word);
64 }
65
66 // Find the longest valid word
67 string result = "";
68 for (const string& word : words) {
69 // Check if current word is better than the result:
70 // 1. Longer length, OR
71 // 2. Same length but lexicographically smaller
72 bool isBetterWord = (word.size() > result.size()) ||
73 (word.size() == result.size() && word < result);
74
75 // Update result if word is better and can be built incrementally
76 if (isBetterWord && trie.search(word)) {
77 result = word;
78 }
79 }
80
81 return result;
82 }
83};
84
1// Node structure for Trie with children array and end-of-word flag
2interface TrieNode {
3 children: (TrieNode | null)[];
4 isEndOfWord: boolean;
5}
6
7// Create a new Trie node with 26 null children (for lowercase letters a-z)
8function createTrieNode(): TrieNode {
9 return {
10 children: Array(26).fill(null),
11 isEndOfWord: false
12 };
13}
14
15// Insert a word into the Trie
16function insert(root: TrieNode, word: string): void {
17 let currentNode: TrieNode = root;
18
19 // Traverse through each character in the word
20 for (const char of word) {
21 // Calculate the index for the character (0-25 for a-z)
22 const index: number = char.charCodeAt(0) - 'a'.charCodeAt(0);
23
24 // Create a new node if it doesn't exist
25 if (!currentNode.children[index]) {
26 currentNode.children[index] = createTrieNode();
27 }
28
29 // Move to the child node
30 currentNode = currentNode.children[index] as TrieNode;
31 }
32
33 // Mark the end of the word
34 currentNode.isEndOfWord = true;
35}
36
37// Search for a word in the Trie, checking if all prefixes are also words
38function search(root: TrieNode, word: string): boolean {
39 let currentNode: TrieNode = root;
40
41 // Traverse through each character in the word
42 for (const char of word) {
43 // Calculate the index for the character
44 const index: number = char.charCodeAt(0) - 'a'.charCodeAt(0);
45
46 // Move to the child node
47 currentNode = currentNode.children[index] as TrieNode;
48
49 // Check if each prefix is a valid word (requirement for building word chain)
50 if (!currentNode.isEndOfWord) {
51 return false;
52 }
53 }
54
55 return true;
56}
57
58// Find the longest word that can be built one character at a time from other words
59function longestWord(words: string[]): string {
60 // Initialize the Trie root
61 const trieRoot: TrieNode = createTrieNode();
62
63 // Insert all words into the Trie
64 for (const word of words) {
65 insert(trieRoot, word);
66 }
67
68 // Track the longest valid word found
69 let longestValidWord: string = '';
70
71 // Check each word to see if it can be built incrementally
72 for (const word of words) {
73 // Update if current word is longer, or same length but lexicographically smaller
74 const isLonger: boolean = word.length > longestValidWord.length;
75 const isSameLengthButSmaller: boolean = word.length === longestValidWord.length && word < longestValidWord;
76
77 if ((isLonger || isSameLengthButSmaller) && search(trieRoot, word)) {
78 longestValidWord = word;
79 }
80 }
81
82 return longestValidWord;
83}
84
Time and Space Complexity
The time complexity is O(โ_{w โ words} |w|)
, where |w|
represents the length of each word in the words list.
Breaking down the time complexity:
- Building the Trie: We iterate through each word and insert it character by character. This takes
O(โ_{w โ words} |w|)
time since we process every character of every word exactly once. - Searching in the Trie: In the second loop, we again iterate through each word and search for it in the Trie. Each search operation traverses the word character by character, taking
O(|w|)
time. For all words, this is againO(โ_{w โ words} |w|)
. - The total time complexity is
O(โ_{w โ words} |w|) + O(โ_{w โ words} |w|) = O(โ_{w โ words} |w|)
.
The space complexity is O(โ_{w โ words} |w|)
.
Breaking down the space complexity:
- The Trie structure stores all the words. In the worst case, if no words share common prefixes, we need to create a new Trie node for each character of each word.
- Each Trie node contains an array of 26 pointers (for lowercase English letters) and a boolean flag.
- The maximum number of nodes created is proportional to the total number of characters across all words, which is
โ_{w โ words} |w|
. - Therefore, the space complexity is
O(โ_{w โ words} |w|)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Misunderstanding the Prefix Validation Requirement
The Problem:
A common mistake is thinking that we only need to check if prefixes exist in the array, without requiring that every prefix must also satisfy the same building condition. For example, with words = ["a", "banana", "app", "appl", "ap", "apply", "apple"]
, someone might incorrectly think "apple" is valid just because "a", "ap", "app", "appl", and "apple" all exist in the array.
However, the word "appl" itself isn't valid because while "a", "ap", "app" exist, they need to be buildable character by character too. The current implementation correctly handles this by checking is_end
at each step during traversal.
The Fix:
The provided solution correctly addresses this by ensuring during the search
operation that at each character position, we verify is_end = True
, meaning that prefix is a complete valid word that was inserted into the trie.
Pitfall 2: Incorrect Lexicographic Comparison for Same-Length Words
The Problem:
When multiple words have the same maximum length, developers might forget to handle the lexicographic ordering or implement it incorrectly. For instance, they might use >=
instead of >
for length comparison, which could lead to replacing a lexicographically smaller word with a larger one of the same length.
# Incorrect approach:
if len(word) >= len(result) and trie.search(word): # Wrong!
result = word
This would always take the last valid word of maximum length, not the lexicographically smallest.
The Fix: The solution correctly handles this with explicit conditions:
is_longer = len(word) > len(result)
is_same_length_but_smaller = len(word) == len(result) and word < result
if (is_longer or is_same_length_but_smaller) and trie.search(word):
result = word
Pitfall 3: Not Handling Empty Input or Single Character Words
The Problem: Some implementations might not properly handle edge cases like:
- Empty words array:
words = []
- Array with only single-character words:
words = ["a", "b", "c"]
- Words that start with different letters where no building is possible
The Fix: The current solution handles these cases naturally:
- Empty array returns
""
(initialized result) - Single character words are valid if they exist in the array (their only prefix is themselves)
- The search method correctly validates each word independently
Pitfall 4: Memory Optimization Oversight
The Problem: Creating a full 26-element array for each trie node can be wasteful when dealing with sparse data (words that don't use all letters). For very large datasets, this could lead to memory issues.
Alternative Approach for Memory Optimization:
class TrieNode:
def __init__(self):
self.children = {} # Use dictionary instead of array
self.is_end = False
While this uses less memory for sparse tries, it has slightly worse cache locality and might be marginally slower for dense tries. The array approach in the solution is generally preferred for LeetCode-style problems where the alphabet size is fixed and small.
In a binary min heap, the minimum element can be found in:
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
Trie Introduction Imagine you have a magic dictionary where as soon as you start spelling a word it immediately tells you how many words that start with those letters How cool would that be This is exactly the kind of magic a data structure called trie performs What's a Trie
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donโt Miss This!