Problem overview
The function exist receives a grid of letters, board, as a Vec<Vec<char>>, and a word. It returns true if the word can be traced through the grid. A trace starts at any cell and moves one step at a time to a cell directly above, below, left or right, never diagonally. The visited cells must spell the word in order, no cell can be used twice, and letters are case-sensitive.
Take this board:
A B C E
S F C S
A D E E
The word "ABCCED" can be traced as A (0,0), B (0,1), C (0,2), C (1,2), E (2,2), D (2,1), so the answer is true. "SEE" works too: S (1,3), E (2,3), E (2,2). "ABCB" fails. A, B and C are along the top row, but the only B next to that C is the one already used.
Approach: depth-first search with backtracking
Since the word can start anywhere, try every cell as the first letter. From a starting cell, the search is a depth-first search: match the current cell against the current letter, then ask each of the four neighbours to match the next letter.
The rule "each cell at most once" needs memory of the current path. The solution writes it into the board itself: when a cell matches, it is overwritten with '#', which is never a letter, so any deeper step that reaches it fails the letter test. When all four neighbours have been tried, the original letter is put back. That last part is the backtracking: the cell is free again for paths that are tried later and don't go through it here.
Here is the search for "ABCB" starting at the A in the corner:
(0,0) matches A
(-1,0) outside
(1,0) holds S not B
(0,-1) outside
(0,1) matches B
(-1,1) outside
(1,1) holds F not C
(0,0) holds # not C
(0,2) matches C
(-1,2) outside
(1,2) holds C not B
(0,1) holds # not B
(0,3) holds E not B
At the second C, the cell below holds a C, not the B the word needs. The cell to its left, the B the path came from, now holds #, so it can't be used again. The last neighbour holds an E. Every branch has failed, each cell is restored on the way back, and the search moves on to the next starting cell. In the end no start works, and the result is false.
A path dies at its first wrong letter, so the search stays small in practice. The worst case is still large: every cell can start a search, and each step has up to 4 choices (3 after the first, because one neighbour is the cell just left). For a word of length L on R rows and C columns, that is O(R·C·4^L) time.
Step by step
- Turn the word into characters. A Rust
Stringcan't be indexed by position, soword.chars().collect()builds aVec<char>. The newwordhides the parameter for the rest of the function. - Loop over every cell.
rowgoes over0..board.len()andcolover0..board[0].len(). - Start a search from each cell.
Self::dfsgets the board with&mut, the word with&, the position asi32values, and0as the index of the first letter. If it returnstrue, so doesexist. - Return
falseafter the loops. No cell could start the word. - Write the helper's signature.
dfsanswers whether the letters ofwordfromindexon can be traced starting at the cell (row,col). The board is a mutable borrow so the helper can write its marks, and the word is a shared borrow. - Succeed when every letter is matched. If
index == word.len(), the previous cell matched the last letter, so returntrue. This test comes first because the current cell may lie outside the board. - Fail outside the board. A negative coordinate, or one at or beyond the board's size, returns
false. Doing this in the helper saves each caller from checking its neighbours. - Fail on a wrong letter. If the cell isn't
word[index], returnfalse. A cell already on the path holds'#'and fails here too. - Mark the cell with
'#'. - Create
foundasfalse. The cell must be unmarked before returning, so the result can't be returned the moment one neighbour succeeds. - List the four moves as an array of
(row change, column change)tuples: up, down, left and right. - Loop over the moves, unpacking each into
(dr, dc). - Try each neighbour with
found = found || Self::dfs(...), passingindex + 1. The||only evaluates its right side whilefoundis stillfalse, so once one neighbour succeeds, the rest are skipped. - Unmark the cell by writing
word[index], the letter it matched, back into the board. - Return
found.
The solution in Rust
impl Solution {
fn exist(mut board: Vec<Vec<char>>, word: String) -> bool {
let word: Vec<char> = word.chars().collect();
for row in 0..board.len() {
for col in 0..board[0].len() {
if Self::dfs(&mut board, &word, row as i32, col as i32, 0) {
return true;
}
}
}
false
}
fn dfs(board: &mut Vec<Vec<char>>, word: &Vec<char>, row: i32, col: i32, index: usize) -> bool {
if index == word.len() {
return true;
}
if row < 0 || row >= board.len() as i32 || col < 0 || col >= board[0].len() as i32 {
return false;
}
if board[row as usize][col as usize] != word[index] {
return false;
}
board[row as usize][col as usize] = '#';
let mut found = false;
let directions: [(i32, i32); 4] = [(-1, 0), (1, 0), (0, -1), (0, 1)];
for (dr, dc) in directions {
found = found || Self::dfs(board, word, row + dr, col + dc, index + 1);
}
board[row as usize][col as usize] = word[index];
found
}
}
Rust details worth knowing:
- Why
i32for coordinates. The neighbour of row 0 is row -1, which ausizecan't hold. The helper takesi32values, checks the bounds, and only then converts withas usizeto index the board. Subtracting 1 from ausizethat is 0 would panic in a debug build. mut boardin the signature.existowns its copy of the board, andmutlets it lend that copy out as&mut. The caller's board is not affected, and the helper puts every letter back anyway.&mut boardand passing it on. Insidedfs,boardis already a&mut Vec<Vec<char>>, so recursive calls pass it as it is. Rust reborrows it for the call, and the borrow ends when the call returns.- Short-circuit with
||. The right-hand call isn't run oncefoundistrue. The marks are still undone, because the restoring line comes after the loop. - The tuple array. The type
[(i32, i32); 4]is an array of exactly four pairs.for (dr, dc) in directionstakes each pair apart in the loop header. - Comparing
chars.board[row as usize][col as usize] != word[index]compares twocharvalues, and the comparison is case-sensitive: with this board,"abcced"returnsfalse.
Complexity
Time: O(R·C·4^L) in the worst case, for R rows, C columns and a word of length L. There are R·C starting cells, and each search can branch 4 ways at each of up to L letters. Wrong letters and used cells cut most branches long before that.
Space: O(L) extra. The marks live in the board, and the only other memory is the recursion, one call per matched letter. The vector of characters for the word also takes O(L).
Edge cases and common mistakes
- A single cell. A board
[["A"]]with the word"A"givestrue. With the word"AA"it givesfalse, because the only A can't be used twice. - Reusing a cell. On the board of the example,
"ABCB"fails because the only B next to the C has been used. A path may go left or up, though: on a board with the single rowAB, the word"BA"works. - Forgetting to unmark. Without the restoring line, a failed path leaves
#cells behind, and later starting cells find the board damaged. Words that exist are reported as missing. - Testing the index too late. The call that follows the last letter has
index == word.len(). If the letter comparison ran before theindextest, it would readword[word.len()], past the end of the vector, and panic. - Using the word's characters as marks. Words contain only letters, so
'#'is safe as a mark. If a word could contain it, the mark would need to be something else, or a separatevisitedgrid. - Diagonal moves. Only the four side-by-side moves are allowed.
Testing
On the board above:
"ABCCED"givestrue."SEE"givestrue."ABCB"givesfalse."ABCCEDX"givesfalse."abcced"givesfalse.
On other boards, [["A"]] with "A" gives true, [["A"]] with "AA" gives false, and a single row AB with "BA" gives true.
