Facebook Pixel

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 in words โœ“
  • "ap" is in words โœ“
  • "app" is in words โœ“

Important Rules:

  1. 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)
  2. 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:

  1. Building a tree structure (Trie) from all words
  2. For each word, performing a depth-first traversal to verify that every prefix along the path is a valid word
  3. The search operation traverses from root to leaf, checking the is_end flag at each node to ensure all prefixes exist as complete words
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Shared prefix paths: Words with common prefixes share nodes in the tree, avoiding redundant storage
  2. 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 letter
  • is_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:

  1. Start from the root node
  2. 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
  3. 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:

  1. Start from the root node
  2. 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 is True - if not, this prefix doesn't exist as a complete word, return False
  3. If we successfully traverse the entire word with all prefixes valid, return True

Finding the Answer:

  1. Insert all words into the Trie
  2. For each word w in words:
    • 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
  3. 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 Evaluator

Example 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 again O(โˆ‘_{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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the minimum element can be found in:


Recommended Readings

Want a Structured Path to Master System Design Too? Donโ€™t Miss This!

Load More