Problem overview
The method Exist receives board, a char[][] (an array of rows, each row an array of letters), and a string word. It returns true when the word can be traced through the grid and false otherwise.
To trace a word, start at any cell and move one step at a time up, down, left or right, never diagonally. The cells visited must spell the word in order, and no cell may be used twice. Letters are case-sensitive.
Take this board:
A B C E
S F C S
A D E E
The word ABCCED is on the board: A at row 0, column 0, then B, then C along the top row, down to the C in row 1, down to the E in row 2, and left to D. The word SEE is there too, going down from the S in the right column. The word ABCB is not: A, B, C work along the top row, but the only B next to that C is the one already used.
Approach: try every start, extend one letter at a time
The word can start anywhere, so the method tries every cell as the first letter. From a start cell, a depth-first search matches one letter per call: if the cell holds the letter, the search asks the four neighbours to match the rest of the word. A path is abandoned at its first wrong letter, which keeps the search small in practice.
The one subtle point is the rule that a cell can't be reused. The search marks a cell as used while it is on the current path. Once all four neighbours have been tried, the search unmarks the cell, because a different path, tried later, may legitimately pass through it. Marking, exploring and unmarking is what makes this backtracking.
Rather than keeping a separate visited table, the solution overwrites the cell with '#'. The statement guarantees the board contains only letters, so '#' never equals a letter of word, and a marked cell fails the letter test automatically. The cell's real letter is word[index], which is how it gets restored.
Step by step
Exist: try every start
- Loop over rows with
rowfrom 0 whilerow < board.Length. For a jagged array,board.Lengthis the number of rows. - Loop over columns inside it, up to
board[0].Length, the length of the first row. Every row has the same length here. - Call
Dfs(board, word, row, col, 0)and returntrueif it succeeds. The last argument, 0, is the index of the first letter. One successful start is enough. - Return
falseafter both loops, since no start worked.
Dfs: match one letter
- Declare
Dfsas apublic boolmethod taking the board, the word,row,colandindex, the position in the word of the letter this cell must hold. It answers whether the word fromindexto the end can be traced starting here. - If
index == word.Length, returntrue. Every letter has been matched, so this cell doesn't matter. It may even be off the board, which is why this test comes before the bounds test. - Return
falsewhen the cell is off the board: a row or column below 0 or past the last one. Neighbours of edge cells land here, so testing once inDfssaves every caller from checking. - Return
falsewhenboard[row][col]isn'tword[index]. A wrong letter and an already used cell (holding'#') both stop here. - Mark the cell by setting it to
'#'. - Create
foundasfalse. The method can't return as soon as a neighbour succeeds, because the cell must be unmarked first. - Describe the four moves in an
int[][]nameddirections:{-1, 0}up,{1, 0}down,{0, -1}left,{0, 1}right. One loop then replaces four copies of the same call. - Loop over
directionswithforeach (int[] d in directions), whered[0]is the row change andd[1]the column change. - Update
foundwithfound || Dfs(...)on the neighbour, withindex + 1for the next letter. The||operator skips its right side oncefoundistrue, so no more neighbours are searched after a success. - Unmark the cell after the loop by writing
word[index]back into it. - Return
found.
The solution in C#
class Solution {
public bool Exist(char[][] board, string word) {
for (int row = 0; row < board.Length; row++) {
for (int col = 0; col < board[0].Length; col++) {
if (Dfs(board, word, row, col, 0)) {
return true;
}
}
}
return false;
}
public bool Dfs(char[][] board, string word, int row, int col, int 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] = '#';
bool found = false;
int[][] directions = new int[][] { new int[]{-1, 0}, new int[]{1, 0}, new int[]{0, -1}, new int[]{0, 1} };
foreach (int[] d in directions) {
found = found || Dfs(board, word, row + d[0], col + d[1], index + 1);
}
board[row][col] = word[index];
return found;
}
}
The C# details worth knowing:
- Jagged array.
char[][]is an array ofchar[]rows, indexedboard[row][col]. This is different from a rectangularchar[,], which is indexedboard[row, col]and measured withGetLength. - Characters compare by value.
board[row][col] != word[index]compares twocharvalues, and indexing astringgives achar. Single quotes make achar('#'), double quotes astring. - Arrays are references. The board is changed in place during the search, and every recursive call sees the same array. Because each mark is undone, the board is exactly as it was when
Existreturns, even when the answer isfalse. - Short-circuit evaluation. In
found || Dfs(...), the call on the right runs only whilefoundisfalse. Writingfound |= Dfs(...)would run all four calls even after a success, which still gives the right answer but wastes work. - Order of the tests. The base case, the bounds test and the letter test must run in that order. Reading
board[row][col]before the bounds test throwsIndexOutOfRangeExceptionat the edges.
Complexity
Let R and C be the board's dimensions and L the length of word.
Time: O(R * C * 4^L) in the worst case. There are R * C starting cells. From each, a path has up to L letters, and each step has at most 4 neighbours (3 after the first, since the cell it came from is marked). In practice most paths stop at the first wrong letter, so the search is much faster than this bound.
Space: O(L). There is one active call per matched letter, so the recursion is at most L calls deep. The marks live in the board itself, so there is no visited table.
Edge cases and common mistakes
- A one-cell board. With the board
{"A"}, the wordAis found and the wordAAis not, because the single cell can't be used twice. - Reusing a cell. Forgetting to mark cells lets a path go back and forth between two cells that hold the same letter. In the board
{"AA"}, the wordAAAwould wrongly succeed. With marking, it correctly returnsfalse. - Forgetting to unmark. A cell left as
'#'after a failed path blocks every later path through it, and valid words are missed. The board is also corrupted for the next call. - Returning early. Writing
return Dfs(...)inside the loop for the first direction skips the other three directions and the unmarking. - Checking the base case after the bounds. On the last letter, the final recursive call may be off the board, and a bounds test first would turn a complete match into
false. - Case.
aandAare different letters, so the wordais not on the board above.
Testing
On the board {"ABCE", "SFCS", "ADEE"}:
ABCCEDgivestrue.SEEgivestrue.ABCBgivesfalse.Egivestrue.agivesfalse.
After each of these calls, the board still reads ABCE, SFCS, ADEE.
