Problem overview
The method exist receives board, a vector<vector<char>> of letters, and a string word. It returns true if the word can be traced through the grid and false otherwise. A path may start in any cell and moves up, down, left or right, never diagonally. Each step must land on the next letter of the word, and no cell may be used twice in one path. Letters are case-sensitive.
The board in the examples is:
ABCE
SFCS
ADEE
"ABCCED" is found by going right along the top row to A, B, C, then down to the C below, down to E, and left to D. "SEE" is found at the right edge: S, then the E under it, then the E to its left. "ABCB" is not found: the path A, B, C works, but the only B next to that C is the one already used.
Approach: try every start, extend one letter at a time, undo
The grid is small (at most 6 by 6) and the word is short (at most 15 letters), so a search that tries paths one after another is enough. The shape of the search is backtracking: a depth-first search that makes a choice, explores what follows, and then takes the choice back so that other paths can use the same cell.
One call of dfs answers a small question: starting at this cell, can the letters of word from position index to the end be traced? The call checks that the cell holds word[index]. If it does, it marks the cell as used and asks each of the four neighbours the same question for index + 1. Before returning it puts the cell's letter back.
The word may begin anywhere, so exist calls dfs from every cell with index 0 and returns true at the first success.
The search stays small because a path stops at its first wrong letter. In the worst case the search from each of the R*C cells branches into four directions for each of the L letters (three after the first, since the cell just left is in use), so the bound is O(R*C*4^L), but real boards cut most branches after a letter or two.
Step by step
- Loop over every row from 0 while
row < board.size(). - Loop over every column inside it, up to
board[0].size(). Every row has the same length, so the first row's length works for all. - Start a search at
(row, col)with index 0. Ifdfsreturnstrue, returntrue: one successful start is enough. - Return
falseafter both loops, since no cell could start the word. - Declare
dfswith the board and word as references, the cell, and theindexof the letter this cell must hold. - Test success first: if
index == word.size(), every letter has been matched, so returntrue. The cell that matched the last letter already did the work, and the current cell may even be off the board, which is why this test comes before the bounds test. - Reject cells outside the board: a row or column below 0 or past the last one returns
false. Neighbours of edge cells land here. - Reject a wrong letter: if
board[row][col]differs fromword[index], returnfalse. A cell already on the path holds'#', which is never a letter, so this test also rejects reused cells. - Mark the cell by writing
'#'into it. - Prepare a
bool found, starting false, and the four moves as pairs of (row change, column change): up, down, left, right. - Try each move in a loop that unpacks the pair as
auto [dr, dc], and combine the results intofoundwith||. - Unmark the cell by writing
word[index]back. This is the backtracking step. - Return
found. It is not returned earlier, because the cell must be restored before the call ends.
The solution in C++
class Solution {
public:
bool exist(vector<vector<char>>& board, string word) {
for (int row = 0; row < board.size(); row++) {
for (int col = 0; col < board[0].size(); col++) {
if (dfs(board, word, row, col, 0)) {
return true;
}
}
}
return false;
}
bool dfs(vector<vector<char>>& board, string& word, int row, int col, int index) {
if (index == word.size()) {
return true;
}
if (row < 0 || row >= board.size() || col < 0 || col >= board[0].size()) {
return false;
}
if (board[row][col] != word[index]) {
return false;
}
board[row][col] = '#';
bool found = false;
vector<pair<int, int>> directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
for (auto [dr, dc] : directions) {
found = found || dfs(board, word, row + dr, col + dc, index + 1);
}
board[row][col] = word[index];
return found;
}
};
C++ details worth a closer look:
- References are essential for the board.
dfstakesvector<vector<char>>&, so every call marks and unmarks the one shared board. A by-value board would be copied at every call, which is slow, and the marks would not be seen by the callers. - Short-circuit
||. Infound || dfs(...), the right side runs only whilefoundis stillfalse. Once one neighbour completes the word, the other neighbours are skipped. - Structured bindings.
auto [dr, dc]splits eachpair<int, int>into two names. It needs C++17, which the lessons use. - Order inside the class.
existcallsdfsbeforedfsis written. Inside a class body that is fine: member functions can use each other whatever the order. - Signed and unsigned.
board.size()is unsigned. Comparingrow >= board.size()convertsrowto unsigned, and a negativerowwould become a huge number, so this particular test would still reject it. The explicitrow < 0keeps the intent obvious. Compilers may warn about comparing signed and unsigned values. - The
'#'marker. It works because the board holds only English letters. A board that could contain'#'would need a separate visited array.
Complexity
Time: O(R*C*4^L) in the worst case, with R rows, C columns and a word of L letters. There are R*C starting cells, and a path of L letters branches at most four ways per step (three after the first). Matching stops at the first wrong letter, so most inputs do much less work.
Space: O(L) besides the board. The recursion is at most L calls deep, one per matched letter. The marks are written into the board itself, so no visited array is needed.
Edge cases and common mistakes
- Forgetting to unmark. Without the restore step, cells stay as
'#'after a failed path, and later paths cannot use them. A word that needs one of those cells is wrongly reported as missing. With the restore step, the board is unchanged afterexistreturns. - Checking bounds after reading the cell.
board[row][col]withrow = -1orcolpast the end is undefined behaviour. The bounds test has to come first. - Testing the index after the bounds. When the last letter matches at a cell on the edge, the next call is made on a neighbour that may be outside the board. If the bounds test came before the
index == word.size()test, a word ending on the edge would be rejected. - Reusing a cell. Without the
'#'mark,"ABCB"would be found by going A, B, C and back to the first B. - Diagonal moves. Only the four directions in
directionsare allowed. - A word longer than the board. A 1 by 1 board with
"aa"givesfalse, because the only cell would be used twice. - Case.
aandAare different letters, and the comparison is a plain!=onchar.
Testing
With the board above:
"ABCCED"givestrue."SEE"givestrue."ABCB"givesfalse."a"givesfalse, since the board has only capital letters."ASADFBCCEESE"givestrue(a path that uses every one of the 12 cells).
On other boards:
- A board with the single cell
aand the word"a"givestrue, while the word"aa"givesfalse. - A board with one row,
a b, and the word"ba"givestrue, while"aba"givesfalse.
