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 thek
-th column for all valid positionsk
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:
- Building a Trie that stores all words and tracks which words have each prefix
- Using DFS (depth-first search) to build word squares row by row
- For each partially built square, determining what prefix the next word must have by looking at the vertical columns formed so far
- Using the Trie to quickly find all words matching that required prefix
- 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.
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:
- First word: Can be any word from our list
- Second word: Must start with the 2nd character of the first word
- Third word: Must start with the 3rd character of the first word AND the 3rd character of the second word
- 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 nodev
: A list storing indices of all words that pass through this nodeinsert(w, i)
: Adds wordw
with indexi
to the Trie. As we traverse each character, we add the word's index to every node along the pathsearch(w)
: Returns indices of all words having prefixw
. If the prefix doesn't exist, returns an empty list
Main Algorithm
The wordSquares
function orchestrates the solution:
-
Initialize the Trie: Insert all words with their indices
for i, w in enumerate(words): trie.insert(w, i)
-
Try each word as the first row: Since any word can start a word square
for w in words: dfs([w])
-
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
-
Calculate Required Prefix: For the next word at position
idx
, extract theidx
-th character from each previously placed wordidx = len(t) pref = [v[idx] for v in t] # Get vertical column
For example, if
t = ["ball", "area"]
andidx = 2
, thenpref = ['l', 'e']
, so the next word must start with "le" -
Find Matching Words: Use the Trie to get all words with the required prefix
indexes = trie.search(''.join(pref))
-
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 EvaluatorExample 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 theN
words into the Trie, and each insertion takesO(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 theL
positions, we might need to try multiple words, and for each attempt, we doO(L)
work for prefix search.
- At each level of recursion (from 0 to
Space Complexity: O(N * L * L + L)
-
Trie Storage:
O(N * L * L)
- The Trie stores all
N
words, each of lengthL
. - 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 allN
words, leading toN
indices stored at depthk
. - 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.
- The Trie stores all
-
Recursion Stack:
O(L)
- The maximum depth of recursion isL
(the length of words). -
Output Storage:
O(K * L^2)
whereK
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)
Which of these properties could exist for a graph but not a tree?
Recommended Readings
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
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
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!