Problem overview
The method exist(board, word) receives a grid of letters, an array of rows where each row is an array of one-letter strings, and a string word. It returns true if the word can be traced through the grid and false otherwise. A trace starts at any cell and moves to a side-by-side neighbour (up, down, left or right) for each following letter. A cell can be used at most once in one trace, and letters are case-sensitive.
With the board [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], the word "ABCCED" is found: A at (0,0), B at (0,1), C at (0,2), C at (1,2), E at (2,2), D at (2,1). The word "SEE" is found too. The word "ABCB" is not: the only B next to the C in the top row is the one the path already used.
Approach: try every start, then backtrack
The word can begin in any cell, so the method tries every cell as the first letter. From a starting cell, a recursive helper dfs matches one letter, then asks the four neighbours to match the rest. That is a depth-first search.
Each cell may be used only once on a path, so the helper needs to remember which cells are on the current path. The lesson does this in place: when a cell matches, it is overwritten with '#', which is never a letter, so any deeper call that reaches that cell fails its comparison. When the search comes back from the neighbours, the cell gets its letter back. That undo is the backtracking step: a different path, tried later, may use the cell again.
A path stops at its first wrong letter, so in practice most branches die after a few cells. In the worst case the search is O(R·C·4^L), where R and C are the board's dimensions and L is the word's length. The constraints (at most 6 by 6 and 15 letters) keep that manageable.
Step by step
exist
- Loop over every row with
row, and inside it over every column withcol, up toboard[0].length. - At each cell, call
this.dfs(board, word, row, col, 0), asking whether the word can be traced starting here with index 0, the first letter. If so, returntrueat once, because one working start is enough. - After both loops, return
false: no cell could start the word.
dfs
- If
indexequalsword.length, every letter has been matched, so returntrue. This check comes first because the cell just stepped onto may be outside the board: the previous cell matched the last letter, and this one doesn't matter. - If the cell is outside the board (a negative row or column, or one past the last), return
false. Testing it here means callers don't have to check their neighbours. - If the cell's letter isn't
word[index], returnfalse. A cell already on the path holds'#', so it fails here too. - Mark the cell by setting it to
'#'. - Declare
foundasfalse. The method can't return as soon as a neighbour succeeds, because the cell must be restored first. - Write the four moves as
[row change, column change]pairs indirections, and loop over them, unpacking each pair intodranddc. - In the loop, set
foundtofound ||the result ofdfson the neighbour withindex + 1. - Put
word[index]back into the cell, then returnfound.
The solution in JavaScript
class Solution {
exist(board, word) {
for (let row = 0; row < board.length; row++) {
for (let col = 0; col < board[0].length; col++) {
if (this.dfs(board, word, row, col, 0)) {
return true;
}
}
}
return false;
}
dfs(board, word, row, col, index) {
if (index === word.length) {
return true;
}
if (row < 0 || row >= board.length || col < 0 || col >= board[0].length) {
return false;
}
if (board[row][col] !== word[index]) {
return false;
}
board[row][col] = '#';
let found = false;
const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]];
for (const [dr, dc] of directions) {
found = found || this.dfs(board, word, row + dr, col + dc, index + 1);
}
board[row][col] = word[index];
return found;
}
}
The JavaScript details worth knowing:
- Array destructuring in
for...of.for (const [dr, dc] of directions)takes each two-number array apart intodranddc. - Short-circuit
||.found || this.dfs(...)evaluates the right side only whilefoundisfalse. Once one neighbour finishes the word, the remaining directions are skipped, but the loop still reaches the line that restores the cell. - The board is changed and restored. The method writes
'#'into the array it was given, so the caller's board is briefly modified. Every'#'is replaced before the method returns, which is why the board is the same afterwards: running"ABCB"on the example board and printing it afterwards shows the original three rows. - Strings are indexable.
word[index]gives the one-character string at that position, and!==compares it with the board's one-character string. this.dfs. The helper is a method, so it is called throughthis.
Complexity
Time: O(R·C·4^L) in the worst case. There are R·C starting cells, and each search goes at most L letters deep with up to 4 choices per level (3 after the first, since the cell it came from is marked). Wrong letters cut most branches early.
Space: O(L). The recursion is at most L calls deep. The marking is done in the board itself, so no visited array is needed.
Edge cases and common mistakes
- Reusing a cell. On a board holding
[["a", "a"]], the word"aaa"givesfalse, because only two cells exist. Without the marking, the search would bounce between them. - Forgetting to unmark. If the cell isn't restored, a failed path leaves
'#'behind, and later paths can no longer use those cells. On the board[["a","a","b"],["b","b","a"]], the word"abaa"is traceable, but a version without the restore line returnsfalsefor it. - Testing the index too late. The call that follows the last matched letter has no letter left to match:
word[index]isundefined, and it may also be outside the board. If theindex === word.lengthcheck came after the other two, every search would fail. - Returning inside the loop. Returning as soon as a neighbour succeeds skips the restore step and leaves
'#'in the board. - Case.
[["a", "A"]]contains"aA", but searching for"aa"givesfalse. - Single cell.
[["a"]]contains"a"but not"aa".
Testing
- The example board with
"ABCCED"givestrue. - The example board with
"SEE"givestrue. - The example board with
"ABCB"givesfalse. - The example board with
"ABFSAD"givestrue: A (0,0), B (0,1), F (1,1), S (1,0), A (2,0), D (2,1). [["a"]]with"a"givestrue, and with"aa"givesfalse.[["a", "a"]]with"aaa"givesfalse.
