0079-单词搜索

Raphael Liu Lv10

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回
false

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例 1:

**输入:** board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
**输出:** true

示例 2:

**输入:** board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
**输出:** true

示例 3:

**输入:** board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
**输出:** false

提示:

  • m == board.length
  • n = board[i].length
  • 1 <= m, n <= 6
  • 1 <= word.length <= 15
  • boardword 仅由大小写英文字母组成

进阶: 你可以使用搜索剪枝的技术来优化解决方案,使其在 board 更大的情况下可以更快解决问题?

方法一:回溯

思路与算法

设函数 $\text{check}(i, j, k)$ 表示判断以网格的 $(i, j)$ 位置出发,能否搜索到单词 $\textit{word}[k..]$,其中 $\textit{word}[k..]$ 表示字符串 $\textit{word}$ 从第 $k$ 个字符开始的后缀子串。如果能搜索到,则返回 $\texttt{true}$,反之返回 $\texttt{false}$。函数 $\text{check}(i, j, k)$ 的执行步骤如下:

  • 如果 $\textit{board}[i][j] \neq s[k]$,当前字符不匹配,直接返回 $\texttt{false}$。
  • 如果当前已经访问到字符串的末尾,且对应字符依然匹配,此时直接返回 $\texttt{true}$。
  • 否则,遍历当前位置的所有相邻位置。如果从某个相邻位置出发,能够搜索到子串 $\textit{word}[k+1..]$,则返回 $\texttt{true}$,否则返回 $\texttt{false}$。

这样,我们对每一个位置 $(i,j)$ 都调用函数 $\text{check}(i, j, 0)$ 进行检查:只要有一处返回 $\texttt{true}$,就说明网格中能够找到相应的单词,否则说明不能找到。

为了防止重复遍历相同的位置,需要额外维护一个与 $\textit{board}$ 等大的 $\textit{visited}$ 数组,用于标识每个位置是否被访问过。每次遍历相邻位置时,需要跳过已经被访问的位置。

代码

[sol1-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Solution {
public boolean exist(char[][] board, String word) {
int h = board.length, w = board[0].length;
boolean[][] visited = new boolean[h][w];
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
boolean flag = check(board, visited, i, j, word, 0);
if (flag) {
return true;
}
}
}
return false;
}

public boolean check(char[][] board, boolean[][] visited, int i, int j, String s, int k) {
if (board[i][j] != s.charAt(k)) {
return false;
} else if (k == s.length() - 1) {
return true;
}
visited[i][j] = true;
int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
boolean result = false;
for (int[] dir : directions) {
int newi = i + dir[0], newj = j + dir[1];
if (newi >= 0 && newi < board.length && newj >= 0 && newj < board[0].length) {
if (!visited[newi][newj]) {
boolean flag = check(board, visited, newi, newj, s, k + 1);
if (flag) {
result = true;
break;
}
}
}
}
visited[i][j] = false;
return result;
}
}
[sol1-Python3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]

def check(i: int, j: int, k: int) -> bool:
if board[i][j] != word[k]:
return False
if k == len(word) - 1:
return True

visited.add((i, j))
result = False
for di, dj in directions:
newi, newj = i + di, j + dj
if 0 <= newi < len(board) and 0 <= newj < len(board[0]):
if (newi, newj) not in visited:
if check(newi, newj, k + 1):
result = True
break

visited.remove((i, j))
return result

h, w = len(board), len(board[0])
visited = set()
for i in range(h):
for j in range(w):
if check(i, j, 0):
return True

return False
[sol1-JavaScript]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var exist = function(board, word) {
const h = board.length, w = board[0].length;
const directions = [[0, 1], [0, -1], [1, 0], [-1, 0]];
const visited = new Array(h);
for (let i = 0; i < visited.length; ++i) {
visited[i] = new Array(w).fill(false);
}
const check = (i, j, s, k) => {
if (board[i][j] != s.charAt(k)) {
return false;
} else if (k == s.length - 1) {
return true;
}
visited[i][j] = true;
let result = false;
for (const [dx, dy] of directions) {
let newi = i + dx, newj = j + dy;
if (newi >= 0 && newi < h && newj >= 0 && newj < w) {
if (!visited[newi][newj]) {
const flag = check(newi, newj, s, k + 1);
if (flag) {
result = true;
break;
}
}
}
}
visited[i][j] = false;
return result;
}

for (let i = 0; i < h; i++) {
for (let j = 0; j < w; j++) {
const flag = check(i, j, word, 0);
if (flag) {
return true;
}
}
}
return false;
};
[sol1-Golang]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
type pair struct{ x, y int }

var directions = []pair{{-1, 0}, {1, 0}, {0, -1}, {0, 1}} // 上下左右

func exist(board [][]byte, word string) bool {
h, w := len(board), len(board[0])
vis := make([][]bool, h)
for i := range vis {
vis[i] = make([]bool, w)
}
var check func(i, j, k int) bool
check = func(i, j, k int) bool {
if board[i][j] != word[k] { // 剪枝:当前字符不匹配
return false
}
if k == len(word)-1 { // 单词存在于网格中
return true
}
vis[i][j] = true
defer func() { vis[i][j] = false }() // 回溯时还原已访问的单元格
for _, dir := range directions {
if newI, newJ := i+dir.x, j+dir.y; 0 <= newI && newI < h && 0 <= newJ && newJ < w && !vis[newI][newJ] {
if check(newI, newJ, k+1) {
return true
}
}
}
return false
}
for i, row := range board {
for j := range row {
if check(i, j, 0) {
return true
}
}
}
return false
}
[sol1-C]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
int directions[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};

bool check(char** board, int boardSize, int boardColSize, int** visited, int i, int j, char* s, int sSize, int k) {
if (board[i][j] != s[k]) {
return false;
} else if (k == sSize - 1) {
return true;
}
visited[i][j] = true;
bool result = false;
for (int sel = 0; sel < 4; sel++) {
int newi = i + directions[sel][0], newj = j + directions[sel][1];
if (newi >= 0 && newi < boardSize && newj >= 0 && newj < boardColSize) {
if (!visited[newi][newj]) {
bool flag = check(board, boardSize, boardColSize, visited, newi, newj, s, sSize, k + 1);
if (flag) {
result = true;
break;
}
}
}
}
visited[i][j] = false;
return result;
}

bool exist(char** board, int boardSize, int* boardColSize, char* word) {
int** visited = malloc(sizeof(int*) * boardSize);
for (int i = 0; i < boardSize; i++) {
visited[i] = malloc(sizeof(int) * boardColSize[0]);
memset(visited[i], 0, sizeof(int) * boardColSize[0]);
}
int wordSize = strlen(word);
for (int i = 0; i < boardSize; i++) {
for (int j = 0; j < boardColSize[0]; j++) {
bool flag = check(board, boardSize, boardColSize[0], visited, i, j, word, wordSize, 0);
if (flag) {
return true;
}
}
}
return false;
}

复杂度分析

  • 时间复杂度:一个非常宽松的上界为 $O(MN \cdot 3^L)$,其中 $M, N$ 为网格的长度与宽度,$L$ 为字符串 $\textit{word}$ 的长度。在每次调用函数 $\text{check}$ 时,除了第一次可以进入 $4$ 个分支以外,其余时间我们最多会进入 $3$ 个分支(因为每个位置只能使用一次,所以走过来的分支没法走回去)。由于单词长为 $L$,故 $\text{check}(i, j, 0)$ 的时间复杂度为 $O(3^L)$,而我们要执行 $O(MN)$ 次检查。然而,由于剪枝的存在,我们在遇到不匹配或已访问的字符时会提前退出,终止递归流程。因此,实际的时间复杂度会远远小于 $\Theta(MN \cdot 3^L)$。

  • 空间复杂度:$O(MN)$。我们额外开辟了 $O(MN)$ 的 $\textit{visited}$ 数组,同时栈的深度最大为 $O(\min(L, MN))$。

 Comments
On this page
0079-单词搜索