Problem overview
The method findWords receives a grid of letters, board (a char[][]), and an array of strings, words. It returns every word that can be traced on the board. A word is traced along a path of cells in which each cell touches the previous one horizontally or vertically, never diagonally, and no cell is used twice within one word. Each word that can be traced appears once in the result, in any order. The lesson's tests list the answer alphabetically.
In the first example the board is
o a a n
e t a e
i h k r
i f l v
and the words are oath, pea, eat and rain. The answer is [eat, oath]. The word oath goes from the top-left o right to a, then down to t and down again to h. The board has no p, and no a touches the r, so pea and rain can't be traced.
Approach: one search for all the words
The direct way runs a depth-first search from every cell for every word. Many words share beginnings, so the same paths get walked again and again, once per word.
A trie fixes that. It stores the words as a tree of letters, where all words with the same beginning share the same nodes. A search over the board can then walk the board and the trie together: from a cell, the next letter must be a child of the current trie node, and if it isn't, no word continues this way and the search stops at once. One search from a cell looks for every word at the same time, and it never explores a path that isn't the beginning of some word.
Each search goes at most L cells deep, where L is the length of the longest word, and branches in at most four directions, so the work is O(R · C · 4^L) for a board of R rows and C columns, plus the time to build the trie. The number of words affects only the trie-building cost.
Step by step
- Give
TrieNodeachildrenmap, aMap<Character, TrieNode>starting as an emptyHashMap. It maps each next letter to the child node. Map keys must be objects, so the key type isCharacter, and Java converts betweencharandCharacterby itself. - Give it a
String wordfield, starting asnull. On the node where a word ends, it holds that word. Storing the word itself instead of a yes/no flag lets the search add it to the answer the moment it reaches the node. - Create the trie's root in
findWords. It stands for the empty beginning, so every word starts there. - Insert each word. Loop over the words, start a
nodeat the root, then loop over the word's characters. - Walk down, creating as needed. If the current node has no child for the letter, put a new
TrieNodeunder it. Then move to that child. - Mark the end. After the last letter, set
node.wordto the word. - Prepare the answer. Create the list
found, anArrayList. The helper adds to this one list. - Start a search from every cell. Two nested loops over the rows and columns call
dfs(board, row, col, root, found). A word can begin anywhere. Every row has the same length, soboard[0].lengthis the number of columns. - Return
found. - In
dfs, stop outside the board. The calls for the four neighbours are made without checking them, so from an edge cell some of them land off the board. They return here. - Read the cell's letter into a
charnamedletter, because it's needed again at the end to restore the cell. - Stop if no word continues with this letter. If
node.childrenhas no keyletter, return. This check is what keeps the search small. - Move down the trie to
child, the node for the path so far plus this cell. - Report a finished word. If
child.wordisn'tnull, the path spells that word: add it tofound, then setchild.wordback tonull. A word can often be traced along several paths, and once it is cleared the later paths find nothing to add, so each word is listed once. - Mark the cell as used by writing
'#'into it. No word contains'#', so a call that comes back to this cell stops at thechildrencheck. A path therefore uses each cell at most once. - Search the four neighbours with
childas the trie node: below, above, right, left. From now on the path includes this cell. - Restore the cell by putting
letterback. The cell is free again for other paths. Undoing the mark on the way out is what makes this backtracking.
The solution in Java
class TrieNode {
Map<Character, TrieNode> children = new HashMap<>();
String word = null;
}
class Solution {
public List<String> findWords(char[][] board, String[] words) {
TrieNode root = new TrieNode();
for (String word : words) {
TrieNode node = root;
for (char c : word.toCharArray()) {
if (!node.children.containsKey(c)) {
node.children.put(c, new TrieNode());
}
node = node.children.get(c);
}
node.word = word;
}
List<String> found = new ArrayList<>();
for (int row = 0; row < board.length; row++) {
for (int col = 0; col < board[0].length; col++) {
dfs(board, row, col, root, found);
}
}
return found;
}
void dfs(char[][] board, int row, int col, TrieNode node, List<String> found) {
if (row < 0 || row >= board.length || col < 0 || col >= board[0].length) {
return;
}
char letter = board[row][col];
if (!node.children.containsKey(letter)) {
return;
}
TrieNode child = node.children.get(letter);
if (child.word != null) {
found.add(child.word);
child.word = null;
}
board[row][col] = '#';
dfs(board, row + 1, col, child, found);
dfs(board, row - 1, col, child, found);
dfs(board, row, col + 1, child, found);
dfs(board, row, col - 1, child, found);
board[row][col] = letter;
}
}
Java details worth a closer look:
- A path through the trie, not just the board.
dfstakesnode, the trie node reached before the cell, and passeschilddown to its neighbours. The letter of the cell is looked up innode.children, and its result becomes the node for the next level. - Passing arrays and lists. Java passes the reference to
boardand tofound, so changes made indfsare visible to the caller. That is why writing'#'into a cell works without returning anything, and why the restore on the last line matters: the caller's board would otherwise stay damaged. charkeys.containsKey(c)andput(c, ...)box thecharinto aCharacter. Lookups work becauseCharacterimplementsequalsandhashCodeby value.toCharArray(). It returns a newchar[]with the characters of the string, which the for-each loop reads one at a time.- The order of the result.
foundis in the order the searches discover the words. On the first example it is[oath, eat], because the search meets the top-leftofirst. The tests list the answer alphabetically, so a solution should not rely on its own order. - The board is restored. After
findWordsreturns, every cell holds its original letter again.
Complexity
Time: O(R · C · 4^L) for the search, plus O(W · L) to build the trie for W words. A search from one cell goes down at most L levels and tries four neighbours at each one, and it only continues while the letters match a path in the trie. In practice most searches stop after a step or two.
Space: O(W · L) for the trie, since each word adds at most L nodes, plus O(L) for the recursion. The board is changed in place and restored, so it costs nothing extra.
Edge cases and common mistakes
- Reusing a cell. On
[["a","b"],["c","d"]]the wordabdbneeds the cell withbtwice and can't be traced. Without the'#'mark the search would accept it. - Diagonal neighbours.
adon that board is not traced, becauseaanddtouch only at a corner. - Listing a word twice. On
[["s","e","e"],["n","e","t"]]the wordseecan be traced in two ways, and the answer still has it once. That is the job of settingchild.wordtonull. - Words that start other words. With
seeandseenin the list, the node forseereports its word and the search continues below it. Do not stop at the first word found along a path. - Forgetting to restore the cell. The cell stays
'#'and later searches can't pass through it. Words are missed, and which ones depends on the order of the search. - Looking at a cell before the bounds check. Reading
board[row][col]first throwsArrayIndexOutOfBoundsExceptionfor a neighbour off the board. - Single cells. A board with one letter and the word
agives[a], and the wordaaaon[["a","a"]]gives an empty result, because only two cells exist.
Testing
- The board
["oaan","etae","ihkr","iflv"](one string per row) withoath,pea,eat,raingives[eat, oath]once sorted. - The board
["ab","cd"]withabdb,adgives[]. - The board
["see","net"]withsee,seen,set,net,tee,teengives[net, see, seen, tee]once sorted. - The board
["a"]withagives[a]. - The board
["aa"]withaaagives[]. - The board
["ab"]withab,a,b,bagives[a, ab, b, ba]once sorted.
