Facebook Pixel

425. Word Squares 🔒

Problem Description

This problem asks you to find all possible word squares that can be built from a given array of unique strings.

A word square is a special arrangement of words where:

  • Words are arranged in a square grid (same number of rows and columns)
  • The k-th row reads the same as the k-th column for all valid positions k

For example, with the words ["ball","area","lead","lady"]:

b a l l
a r e a
l e a d
l a d y
  • Row 0: "ball", Column 0: "ball" (reading downward) ✓
  • Row 1: "area", Column 1: "area" ✓
  • Row 2: "lead", Column 2: "lead" ✓
  • Row 3: "lady", Column 3: "lady" ✓

Key constraints:

  • All input words are unique
  • The same word can be used multiple times in different word squares
  • All words in the input have the same length
  • You need to return all valid word squares that can be formed

The solution uses a Trie data structure to efficiently find words with specific prefixes. The algorithm works by:

  1. Building a Trie that stores all words and tracks which words have each prefix
  2. Using DFS (depth-first search) to build word squares row by row
  3. For each partially built square, determining what prefix the next word must have by looking at the vertical columns formed so far
  4. Using the Trie to quickly find all words matching that required prefix
  5. Recursively trying each matching word until a complete square is formed

The dfs function builds squares incrementally - when adding the idx-th word, it must have a prefix that matches the idx-th character of all previously added words (to maintain the row-column symmetry property).

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • No: While we use a Trie (which is a tree structure), the core problem is not about traversing a graph with nodes and edges. We're building word arrangements, not exploring graph relationships.

Need to solve for kth smallest/largest?

  • No: We need to find ALL valid word squares, not the kth smallest or largest of something.

Involves Linked Lists?

  • No: The problem deals with arrays of strings and building word squares, not linked list manipulation.

Does the problem have small constraints?

  • Yes: Typically, word square problems have relatively small constraints because:
    • The number of words is usually limited (often ≤ 1000)
    • Word lengths are typically small (often ≤ 5)
    • We need to explore all possible combinations to build valid squares
    • The search space grows exponentially, making it feasible only for small inputs

Brute force / Backtracking?

  • Yes: Backtracking is the perfect fit because:
    • We need to explore all possible ways to build word squares
    • We build the solution incrementally (one row at a time)
    • We need to backtrack when a partial solution cannot lead to a valid complete square
    • We can prune invalid paths early using the Trie to check prefix constraints

Conclusion: The flowchart correctly leads us to use a Backtracking approach. The solution builds word squares row by row, using a Trie for efficient prefix matching, and backtracks whenever the current partial square cannot be extended to a valid complete square.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that a word square has a special symmetry property: the k-th row must equal the k-th column. This constraint becomes our guiding principle.

When building a word square row by row, each new word we add must satisfy increasingly specific prefix requirements. Consider what happens as we build:

  1. First word: Can be any word from our list
  2. Second word: Must start with the 2nd character of the first word
  3. Third word: Must start with the 3rd character of the first word AND the 3rd character of the second word
  4. Fourth word: Must start with specific characters from all previous words

This pattern reveals that as we go deeper, the prefix constraint for the next word becomes more restrictive - it's determined by looking at the vertical columns we've built so far.

For example, if we've placed:

b a l l
a r e a

The third word must start with "le" (the 3rd characters of "ball" and "area").

This naturally suggests a backtracking approach where:

  • We try placing words one by one
  • For each position, we calculate what prefix the next word must have
  • We find all words matching that prefix
  • We recursively try each candidate

The challenge is efficiently finding words with specific prefixes. A naive approach would scan all words each time, but that's inefficient. This is where the Trie comes in - it's the perfect data structure for prefix matching. By preprocessing all words into a Trie, we can quickly retrieve all words with any given prefix in O(prefix_length) time.

The combination of backtracking (to explore all possibilities) and Trie (for efficient prefix matching) gives us an elegant solution that prunes invalid paths early while systematically exploring all valid word square configurations.

Solution Approach

The solution implements a backtracking algorithm with a Trie data structure for efficient prefix searching.

Trie Implementation

The Trie class stores all words and allows quick prefix lookups:

  • children: An array of 26 elements (for 'a' to 'z'), where each element points to the next Trie node
  • v: A list storing indices of all words that pass through this node
  • insert(w, i): Adds word w with index i to the Trie. As we traverse each character, we add the word's index to every node along the path
  • search(w): Returns indices of all words having prefix w. If the prefix doesn't exist, returns an empty list

Main Algorithm

The wordSquares function orchestrates the solution:

  1. Initialize the Trie: Insert all words with their indices

    for i, w in enumerate(words):
        trie.insert(w, i)
  2. Try each word as the first row: Since any word can start a word square

    for w in words:
        dfs([w])
  3. DFS Backtracking Function: The core logic that builds word squares

    def dfs(t):
        if len(t) == len(words[0]):  # Complete square formed
            ans.append(t[:])
            return
  4. Calculate Required Prefix: For the next word at position idx, extract the idx-th character from each previously placed word

    idx = len(t)
    pref = [v[idx] for v in t]  # Get vertical column

    For example, if t = ["ball", "area"] and idx = 2, then pref = ['l', 'e'], so the next word must start with "le"

  5. Find Matching Words: Use the Trie to get all words with the required prefix

    indexes = trie.search(''.join(pref))
  6. Try Each Candidate: Recursively attempt to place each matching word

    for i in indexes:
        t.append(words[i])
        dfs(t)
        t.pop()  # Backtrack

Why This Works

The algorithm exploits the word square property that row k equals column k. When we've placed k words, the (k+1)-th word's prefix is fully determined by the vertical reading of the first k words at position k. The Trie enables O(m) prefix lookup where m is the word length, making the constraint checking efficient.

The backtracking ensures we explore all valid possibilities while pruning invalid branches early when no words match the required prefix.

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 building word squares with words = ["ball", "area", "lead", "lady"].

Step 1: Build the Trie We insert all words into the Trie. Each node stores indices of words that pass through it:

  • "ball" (index 0): b→a→l→l stores [0] at each node
  • "area" (index 1): a→r→e→a stores [1] at each node
  • "lead" (index 2): l→e→a→d stores [2] at each node
  • "lady" (index 3): l→a→d→y stores [3] at each node

Step 2: Start DFS with "ball" as first word

  • Current square: ["ball"]
  • Need 2nd word (idx=1) with prefix = 1st char of each word in square = "a"
  • Trie search for "a" returns indices [1] (only "area" starts with "a")

Step 3: Add "area" as second word

  • Current square: ["ball", "area"]
  • Need 3rd word (idx=2) with prefix = 2nd char of each word = "le"
  • Trie search for "le" returns indices [2] (only "lead" starts with "le")

Step 4: Add "lead" as third word

  • Current square: ["ball", "area", "lead"]
  • Need 4th word (idx=3) with prefix = 3rd char of each word = "lad"
  • Trie search for "lad" returns indices [3] (only "lady" starts with "lad")

Step 5: Add "lady" as fourth word

  • Current square: ["ball", "area", "lead", "lady"]
  • Length equals 4 (word length), so we found a complete word square!

Verification of the word square property:

b a l l    Row 0: "ball"
a r e a    Row 1: "area"  
l e a d    Row 2: "lead"
l a d y    Row 3: "lady"

Column 0: "ball" ✓
Column 1: "area" ✓
Column 2: "lead" ✓
Column 3: "lady" ✓

The algorithm continues backtracking to try other starting words. For instance, starting with "area" would fail at step 2 because no word starts with "r" (the 2nd character of "area"). Starting with "lead" or "lady" would also fail early due to prefix constraints.

The key insight: at each step, the prefix requirement becomes more specific, naturally pruning invalid paths. The Trie makes finding words with required prefixes efficient, turning what could be an O(n) scan into an O(m) lookup where m is the word length.

Solution Implementation

1class Trie:
2    def __init__(self):
3        # Initialize children array for 26 lowercase letters
4        self.children = [None] * 26
5        # Store indices of words that pass through this node
6        self.word_indices = []
7
8    def insert(self, word: str, index: int) -> None:
9        """Insert a word into the trie with its index in the words list."""
10        current_node = self
11      
12        for char in word:
13            # Calculate index for the character (0-25)
14            char_index = ord(char) - ord('a')
15          
16            # Create new node if path doesn't exist
17            if current_node.children[char_index] is None:
18                current_node.children[char_index] = Trie()
19          
20            # Move to child node
21            current_node = current_node.children[char_index]
22            # Store the word index at this node (for prefix matching)
23            current_node.word_indices.append(index)
24
25    def search(self, prefix: str) -> List[int]:
26        """Search for all word indices that have the given prefix."""
27        current_node = self
28      
29        for char in prefix:
30            # Calculate index for the character (0-25)
31            char_index = ord(char) - ord('a')
32          
33            # If path doesn't exist, no words with this prefix
34            if current_node.children[char_index] is None:
35                return []
36          
37            # Move to child node
38            current_node = current_node.children[char_index]
39      
40        # Return all word indices that have this prefix
41        return current_node.word_indices
42
43
44class Solution:
45    def wordSquares(self, words: List[str]) -> List[List[str]]:
46        """
47        Find all possible word squares from the given list of words.
48        A word square is a sequence of words where kth row and column read the same string.
49        """
50      
51        def backtrack(current_square: List[str]) -> None:
52            """Recursively build word squares using backtracking."""
53            # Base case: square is complete when we have n words (n = word length)
54            if len(current_square) == word_length:
55                # Add a copy of the current square to results
56                result.append(current_square[:])
57                return
58          
59            # Get the index for the next word to add
60            next_word_index = len(current_square)
61          
62            # Build prefix for the next word by collecting characters at position 'next_word_index'
63            # from all words currently in the square
64            prefix_chars = [word[next_word_index] for word in current_square]
65            prefix = ''.join(prefix_chars)
66          
67            # Find all words that start with this prefix
68            candidate_indices = trie.search(prefix)
69          
70            # Try each candidate word
71            for word_index in candidate_indices:
72                current_square.append(words[word_index])
73                backtrack(current_square)
74                current_square.pop()  # Backtrack
75      
76        # Initialize trie and result list
77        trie = Trie()
78        result = []
79        word_length = len(words[0])
80      
81        # Build trie with all words and their indices
82        for index, word in enumerate(words):
83            trie.insert(word, index)
84      
85        # Try starting with each word
86        for word in words:
87            backtrack([word])
88      
89        return result
90
1/**
2 * Trie data structure for efficient prefix searching
3 */
4class Trie {
5    // Array to store child nodes for 26 lowercase letters
6    Trie[] children = new Trie[26];
7    // List to store indices of words that pass through this node
8    List<Integer> wordIndices = new ArrayList<>();
9
10    /**
11     * Inserts a word into the trie and associates it with an index
12     * @param word The word to insert
13     * @param index The index of the word in the original array
14     */
15    void insert(String word, int index) {
16        Trie currentNode = this;
17      
18        for (char character : word.toCharArray()) {
19            // Convert character to array index (0-25)
20            int charIndex = character - 'a';
21          
22            // Create new node if path doesn't exist
23            if (currentNode.children[charIndex] == null) {
24                currentNode.children[charIndex] = new Trie();
25            }
26          
27            // Move to child node
28            currentNode = currentNode.children[charIndex];
29            // Add word index to current node
30            currentNode.wordIndices.add(index);
31        }
32    }
33
34    /**
35     * Searches for all words with the given prefix
36     * @param prefix The prefix to search for
37     * @return List of indices of words with the given prefix
38     */
39    List<Integer> search(String prefix) {
40        Trie currentNode = this;
41      
42        for (char character : prefix.toCharArray()) {
43            // Convert character to array index (0-25)
44            int charIndex = character - 'a';
45          
46            // Return empty list if prefix doesn't exist
47            if (currentNode.children[charIndex] == null) {
48                return Collections.emptyList();
49            }
50          
51            // Move to child node
52            currentNode = currentNode.children[charIndex];
53        }
54      
55        // Return all word indices at the final node
56        return currentNode.wordIndices;
57    }
58}
59
60/**
61 * Solution class for finding all valid word squares
62 */
63class Solution {
64    private Trie trie = new Trie();
65    private String[] words;
66    private List<List<String>> result = new ArrayList<>();
67
68    /**
69     * Finds all valid word squares from the given words
70     * @param words Array of words to form squares from
71     * @return List of all valid word squares
72     */
73    public List<List<String>> wordSquares(String[] words) {
74        this.words = words;
75      
76        // Build trie with all words and their indices
77        for (int i = 0; i < words.length; i++) {
78            trie.insert(words[i], i);
79        }
80
81        // Try each word as the first word of the square
82        List<String> currentSquare = new ArrayList<>();
83        for (String word : words) {
84            currentSquare.add(word);
85            dfs(currentSquare);
86            currentSquare.remove(currentSquare.size() - 1);
87        }
88      
89        return result;
90    }
91
92    /**
93     * Depth-first search to build valid word squares
94     * @param currentSquare Current partial word square being built
95     */
96    private void dfs(List<String> currentSquare) {
97        // Check if we've completed a valid square
98        if (currentSquare.size() == words[0].length()) {
99            result.add(new ArrayList<>(currentSquare));
100            return;
101        }
102      
103        // Get the index for the next word to add
104        int nextWordIndex = currentSquare.size();
105      
106        // Build prefix for the next word by collecting characters at nextWordIndex position
107        StringBuilder prefixBuilder = new StringBuilder();
108        for (String word : currentSquare) {
109            prefixBuilder.append(word.charAt(nextWordIndex));
110        }
111      
112        // Find all words with the required prefix
113        List<Integer> candidateIndices = trie.search(prefixBuilder.toString());
114      
115        // Try each candidate word
116        for (int wordIndex : candidateIndices) {
117            currentSquare.add(words[wordIndex]);
118            dfs(currentSquare);
119            currentSquare.remove(currentSquare.size() - 1);
120        }
121    }
122}
123
1#include <vector>
2#include <string>
3#include <memory>
4
5using namespace std;
6
7/**
8 * Trie data structure for efficient prefix searching
9 */
10class Trie {
11private:
12    // Array to store child nodes for 26 lowercase letters
13    Trie* children[26];
14    // Vector to store indices of words that pass through this node
15    vector<int> wordIndices;
16  
17public:
18    /**
19     * Constructor to initialize the Trie node
20     */
21    Trie() {
22        // Initialize all children pointers to nullptr
23        for (int i = 0; i < 26; i++) {
24            children[i] = nullptr;
25        }
26    }
27  
28    /**
29     * Destructor to clean up dynamically allocated memory
30     */
31    ~Trie() {
32        for (int i = 0; i < 26; i++) {
33            if (children[i] != nullptr) {
34                delete children[i];
35            }
36        }
37    }
38  
39    /**
40     * Inserts a word into the trie and associates it with an index
41     * @param word The word to insert
42     * @param index The index of the word in the original array
43     */
44    void insert(const string& word, int index) {
45        Trie* currentNode = this;
46      
47        for (char character : word) {
48            // Convert character to array index (0-25)
49            int charIndex = character - 'a';
50          
51            // Create new node if path doesn't exist
52            if (currentNode->children[charIndex] == nullptr) {
53                currentNode->children[charIndex] = new Trie();
54            }
55          
56            // Move to child node
57            currentNode = currentNode->children[charIndex];
58            // Add word index to current node
59            currentNode->wordIndices.push_back(index);
60        }
61    }
62  
63    /**
64     * Searches for all words with the given prefix
65     * @param prefix The prefix to search for
66     * @return Vector of indices of words with the given prefix
67     */
68    vector<int> search(const string& prefix) {
69        Trie* currentNode = this;
70      
71        for (char character : prefix) {
72            // Convert character to array index (0-25)
73            int charIndex = character - 'a';
74          
75            // Return empty vector if prefix doesn't exist
76            if (currentNode->children[charIndex] == nullptr) {
77                return vector<int>();
78            }
79          
80            // Move to child node
81            currentNode = currentNode->children[charIndex];
82        }
83      
84        // Return all word indices at the final node
85        return currentNode->wordIndices;
86    }
87};
88
89/**
90 * Solution class for finding all valid word squares
91 */
92class Solution {
93private:
94    Trie* trie;
95    vector<string> words;
96    vector<vector<string>> result;
97  
98    /**
99     * Depth-first search to build valid word squares
100     * @param currentSquare Current partial word square being built
101     */
102    void dfs(vector<string>& currentSquare) {
103        // Check if we've completed a valid square
104        if (currentSquare.size() == words[0].length()) {
105            result.push_back(vector<string>(currentSquare));
106            return;
107        }
108      
109        // Get the index for the next word to add
110        int nextWordIndex = currentSquare.size();
111      
112        // Build prefix for the next word by collecting characters at nextWordIndex position
113        string prefix = "";
114        for (const string& word : currentSquare) {
115            prefix += word[nextWordIndex];
116        }
117      
118        // Find all words with the required prefix
119        vector<int> candidateIndices = trie->search(prefix);
120      
121        // Try each candidate word
122        for (int wordIndex : candidateIndices) {
123            currentSquare.push_back(words[wordIndex]);
124            dfs(currentSquare);
125            currentSquare.pop_back();
126        }
127    }
128  
129public:
130    /**
131     * Constructor
132     */
133    Solution() : trie(nullptr) {}
134  
135    /**
136     * Destructor to clean up the trie
137     */
138    ~Solution() {
139        if (trie != nullptr) {
140            delete trie;
141        }
142    }
143  
144    /**
145     * Finds all valid word squares from the given words
146     * @param words Array of words to form squares from
147     * @return List of all valid word squares
148     */
149    vector<vector<string>> wordSquares(vector<string>& words) {
150        this->words = words;
151        this->result.clear();
152      
153        // Initialize trie
154        if (trie != nullptr) {
155            delete trie;
156        }
157        trie = new Trie();
158      
159        // Build trie with all words and their indices
160        for (int i = 0; i < words.size(); i++) {
161            trie->insert(words[i], i);
162        }
163      
164        // Try each word as the first word of the square
165        vector<string> currentSquare;
166        for (const string& word : words) {
167            currentSquare.push_back(word);
168            dfs(currentSquare);
169            currentSquare.pop_back();
170        }
171      
172        return result;
173    }
174};
175
1/**
2 * Trie node structure for efficient prefix searching
3 */
4interface TrieNode {
5    // Map to store child nodes for lowercase letters
6    children: Map<string, TrieNode>;
7    // Array to store indices of words that pass through this node
8    wordIndices: number[];
9}
10
11/**
12 * Creates a new trie node
13 */
14function createTrieNode(): TrieNode {
15    return {
16        children: new Map<string, TrieNode>(),
17        wordIndices: []
18    };
19}
20
21// Global variables for the solution
22let trieRoot: TrieNode;
23let wordsArray: string[];
24let resultSquares: string[][];
25
26/**
27 * Inserts a word into the trie and associates it with an index
28 * @param word - The word to insert
29 * @param index - The index of the word in the original array
30 */
31function insert(word: string, index: number): void {
32    let currentNode = trieRoot;
33  
34    // Traverse through each character in the word
35    for (const character of word) {
36        // Create new node if path doesn't exist
37        if (!currentNode.children.has(character)) {
38            currentNode.children.set(character, createTrieNode());
39        }
40      
41        // Move to child node
42        currentNode = currentNode.children.get(character)!;
43        // Add word index to current node
44        currentNode.wordIndices.push(index);
45    }
46}
47
48/**
49 * Searches for all words with the given prefix
50 * @param prefix - The prefix to search for
51 * @returns Array of indices of words with the given prefix
52 */
53function search(prefix: string): number[] {
54    let currentNode = trieRoot;
55  
56    // Traverse through each character in the prefix
57    for (const character of prefix) {
58        // Return empty array if prefix doesn't exist
59        if (!currentNode.children.has(character)) {
60            return [];
61        }
62      
63        // Move to child node
64        currentNode = currentNode.children.get(character)!;
65    }
66  
67    // Return all word indices at the final node
68    return currentNode.wordIndices;
69}
70
71/**
72 * Depth-first search to build valid word squares
73 * @param currentSquare - Current partial word square being built
74 */
75function dfs(currentSquare: string[]): void {
76    // Check if we've completed a valid square
77    if (currentSquare.length === wordsArray[0].length) {
78        // Add a copy of the current square to results
79        resultSquares.push([...currentSquare]);
80        return;
81    }
82  
83    // Get the index for the next word to add
84    const nextWordIndex = currentSquare.length;
85  
86    // Build prefix for the next word by collecting characters at nextWordIndex position
87    let prefix = '';
88    for (const word of currentSquare) {
89        prefix += word[nextWordIndex];
90    }
91  
92    // Find all words with the required prefix
93    const candidateIndices = search(prefix);
94  
95    // Try each candidate word
96    for (const wordIndex of candidateIndices) {
97        // Add candidate word to current square
98        currentSquare.push(wordsArray[wordIndex]);
99        // Recursively build the rest of the square
100        dfs(currentSquare);
101        // Backtrack by removing the last word
102        currentSquare.pop();
103    }
104}
105
106/**
107 * Finds all valid word squares from the given words
108 * @param words - Array of words to form squares from
109 * @returns List of all valid word squares
110 */
111function wordSquares(words: string[]): string[][] {
112    // Initialize global variables
113    trieRoot = createTrieNode();
114    wordsArray = words;
115    resultSquares = [];
116  
117    // Build trie with all words and their indices
118    for (let i = 0; i < words.length; i++) {
119        insert(words[i], i);
120    }
121  
122    // Try each word as the first word of the square
123    const currentSquare: string[] = [];
124    for (const word of words) {
125        // Add word as first word of square
126        currentSquare.push(word);
127        // Build the rest of the square
128        dfs(currentSquare);
129        // Backtrack by removing the word
130        currentSquare.pop();
131    }
132  
133    return resultSquares;
134}
135

Time and Space Complexity

Time Complexity: O(N * L + N * L^L) where N is the number of words and L is the length of each word.

  • Building the Trie: O(N * L) - We insert each of the N words into the Trie, and each insertion takes O(L) time to traverse through the word's characters.

  • DFS Search: In the worst case, we explore all possible word squares.

    • At each level of recursion (from 0 to L-1), we need to find words with a specific prefix.
    • The prefix search in Trie takes O(L) time.
    • At each position in the word square, we could potentially have up to N choices (in practice, it's limited by valid prefixes).
    • The maximum depth of recursion is L.
    • In the worst case, we could have O(N^L) different paths, but since we're constrained by valid prefixes at each step, the branching factor is limited.
    • More precisely, the time complexity is O(N * L^L) because at each of the L positions, we might need to try multiple words, and for each attempt, we do O(L) work for prefix search.

Space Complexity: O(N * L * L + L)

  • Trie Storage: O(N * L * L)

    • The Trie stores all N words, each of length L.
    • At each node in the Trie, we store a list v containing indices of words that pass through that node.
    • In the worst case, each prefix of length k could be shared by all N words, leading to N indices stored at depth k.
    • Total space for storing indices: O(N * L) across all nodes.
    • The Trie structure itself with 26 pointers per node: O(26 * N * L) = O(N * L) nodes in worst case.
    • Combined: O(N * L * L) considering the indices stored at each level.
  • Recursion Stack: O(L) - The maximum depth of recursion is L (the length of words).

  • Output Storage: O(K * L^2) where K is the number of valid word squares found (not counted in auxiliary space).

Common Pitfalls

1. Incorrect Prefix Construction for Empty Square

One common mistake is not handling the initial case correctly when the square is empty. When starting to build a word square, developers might try to construct a prefix from an empty list, which would result in an empty string prefix.

Pitfall Code:

def backtrack(current_square):
    # This would fail when current_square is empty
    prefix_chars = [word[next_word_index] for word in current_square]
    prefix = ''.join(prefix_chars)

Solution: Handle the empty square case separately or ensure the logic works for all cases:

def backtrack(current_square):
    if len(current_square) == word_length:
        result.append(current_square[:])
        return
  
    # When square is empty, any word can be the first word
    if not current_square:
        for word in words:
            backtrack([word])
    else:
        next_word_index = len(current_square)
        prefix_chars = [word[next_word_index] for word in current_square]
        prefix = ''.join(prefix_chars)
        # ... continue with prefix search

2. Forgetting to Store Word Indices at Intermediate Nodes

A critical mistake in the Trie implementation is only storing word indices at the final character node, rather than at every node along the path. This would make prefix searches fail.

Pitfall Code:

def insert(self, word: str, index: int) -> None:
    current_node = self
    for i, char in enumerate(word):
        char_index = ord(char) - ord('a')
        if current_node.children[char_index] is None:
            current_node.children[char_index] = Trie()
        current_node = current_node.children[char_index]
        # Wrong: Only add index at the last character
        if i == len(word) - 1:
            current_node.word_indices.append(index)

Solution: Add the word index at every node along the path:

def insert(self, word: str, index: int) -> None:
    current_node = self
    for char in word:
        char_index = ord(char) - ord('a')
        if current_node.children[char_index] is None:
            current_node.children[char_index] = Trie()
        current_node = current_node.children[char_index]
        # Correct: Add index at every node for prefix matching
        current_node.word_indices.append(index)

3. Modifying the Result Without Creating a Copy

When adding a completed word square to the results, forgetting to create a copy leads to all results pointing to the same list object, which gets modified during backtracking.

Pitfall Code:

if len(current_square) == word_length:
    result.append(current_square)  # Wrong: adds reference, not copy
    return

Solution: Always create a copy when storing the result:

if len(current_square) == word_length:
    result.append(current_square[:])  # Correct: creates a copy
    # or result.append(list(current_square))
    return

4. Not Handling Edge Cases

Failing to handle edge cases like empty input or single-character words can cause unexpected behavior.

Pitfall Code:

def wordSquares(self, words: List[str]) -> List[List[str]]:
    # Directly accessing words[0] without checking
    word_length = len(words[0])

Solution: Add proper edge case handling:

def wordSquares(self, words: List[str]) -> List[List[str]]:
    if not words:
        return []
  
    word_length = len(words[0])
    if word_length == 0:
        return []
  
    # Continue with main logic...

5. Inefficient Prefix Building

Building the prefix character by character in a loop when you already know the exact positions needed is inefficient and error-prone.

Pitfall Code:

prefix = ""
for i in range(len(current_square)):
    prefix += current_square[i][next_word_index]

Solution: Use list comprehension for cleaner and more efficient code:

prefix_chars = [word[next_word_index] for word in current_square]
prefix = ''.join(prefix_chars)
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of these properties could exist for a graph but not a tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More