LC 212. 单词搜索 II

题目描述

这是 LeetCode 上的 212. 单词搜索 II ,难度为 困难

给定一个 $m x n$ 二维字符网格 $board$ 和一个单词(字符串)列表 $words$,找出所有同时在二维网格和字典中出现的单词。

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

示例 1:

1
2
3
输入:board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"]

输出:["eat","oath"]

示例 2:

1
2
3
输入:board = [["a","b"],["c","d"]], words = ["abcb"]

输出:[]

提示:

  • m == board.length
  • n == board[i].length
  • 1 <= m, n <= 12
  • board[i][j] 是一个小写英文字母
  • 1 <= words.length <= 3 * $10^4$
  • 1 <= words[i].length <= 10
  • words[i] 由小写英文字母组成
  • words 中的所有字符串互不相同

回溯算法

数据范围只有 $12$,且 words 中出现的单词长度不会超过 $10$,可以考虑使用「回溯算法」。

起始先将所有 words 出现的单词放到 Set 结构中,然后以 board 中的每个点作为起点进行爆搜(由于题目规定在一个单词中每个格子只能被使用一次,因此还需要一个 vis 数组来记录访问过的位置):

  1. 如果当前爆搜到的字符串长度超过 $10$,直接剪枝;
  2. 如果当前搜索到的字符串在 Set 中,则添加到答案(同时了防止下一次再搜索到该字符串,需要将该字符串从 Set 中移除)。

代码:

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
class Solution {
Set<String> set = new HashSet<>();
List<String> ans = new ArrayList<>();
char[][] board;
int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
int n, m;
boolean[][] vis = new boolean[15][15];
public List<String> findWords(char[][] _board, String[] words) {
board = _board;
m = board.length; n = board[0].length;
for (String w : words) set.add(w);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
vis[i][j] = true;
sb.append(board[i][j]);
dfs(i, j, sb);
vis[i][j] = false;
sb.deleteCharAt(sb.length() - 1);
}
}
return ans;
}
void dfs(int i, int j, StringBuilder sb) {
if (sb.length() > 10) return ;
if (set.contains(sb.toString())) {
ans.add(sb.toString());
set.remove(sb.toString());
}
for (int[] d : dirs) {
int dx = i + d[0], dy = j + d[1];
if (dx < 0 || dx >= m || dy < 0 || dy >= n) continue;
if (vis[dx][dy]) continue;
vis[dx][dy] = true;
sb.append(board[dx][dy]);
dfs(dx, dy, sb);
vis[dx][dy] = false;
sb.deleteCharAt(sb.length() - 1);
}
}
}

  • 时间复杂度:共有 $m n$ 个起点,每次能往 $4$ 个方向搜索(不考虑重复搜索问题),且搜索的长度不会超过 $10$。整体复杂度为 $O(m n * 4^{10})$
  • 空间复杂度:$O(\sum_{i=0}^{words.length - 1} words[i].length)$

Trie

在「解法一」中,对于任意一个当前位置 $(i, j)$,我们都不可避免的搜索了四联通的全部方向,这导致了那些无效搜索路径最终只有长度达到 $10$ 才会被剪枝。

要进一步优化我们的搜索过程,需要考虑如何在每一步的搜索中进行剪枝。

我们可以使用 $Trie$ 结构进行建树,对于任意一个当前位置 $(i, j)$ 而言,只有在 $Trie$ 中存在往从字符 $a$ 到 $b$ 的边时,我们才在棋盘上搜索从 $a$ 到 $b$ 的相邻路径。

不了解 $Trie$ 的同学,可以看看这篇题解 (题解) 208. 实现 Trie (前缀树),里面写了两种实现 $Trie$ 的方式。

对于本题,我们可以使用「TrieNode」的方式进行建 $Trie$。

因为 words 里最多有 $10^4$ 个单词,每个单词长度最多为 $10$,如果开成静态数组的话,不考虑共用行的问题,我们需要开一个大小为 $10^5 * 26$ 的大数组,可能会有 TLE 或 MLE 的风险。

与此同时,我们需要将平时建 $TrieNode$ 中的 isEnd 标记属性直接换成记录当前字符 s,这样我们在 DFS 的过程中则无须额外记录当前搜索字符串。

代码:

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
44
45
46
47
48
49
50
51
52
53
class Solution {
class TrieNode {
String s;
TrieNode[] tns = new TrieNode[26];
}
void insert(String s) {
TrieNode p = root;
for (int i = 0; i < s.length(); i++) {
int u = s.charAt(i) - 'a';
if (p.tns[u] == null) p.tns[u] = new TrieNode();
p = p.tns[u];
}
p.s = s;
}
Set<String> set = new HashSet<>();
char[][] board;
int n, m;
TrieNode root = new TrieNode();
int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
boolean[][] vis = new boolean[15][15];
public List<String> findWords(char[][] _board, String[] words) {
board = _board;
m = board.length; n = board[0].length;
for (String w : words) insert(w);
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int u = board[i][j] - 'a';
if (root.tns[u] != null) {
vis[i][j] = true;
dfs(i, j, root.tns[u]);
vis[i][j] = false;
}
}
}
List<String> ans = new ArrayList<>();
for (String s : set) ans.add(s);
return ans;
}
void dfs(int i, int j, TrieNode node) {
if (node.s != null) set.add(node.s);
for (int[] d : dirs) {
int dx = i + d[0], dy = j + d[1];
if (dx < 0 || dx >= m || dy < 0 || dy >= n) continue;
if (vis[dx][dy]) continue;
int u = board[dx][dy] - 'a';
if (node.tns[u] != null) {
vis[dx][dy] = true;
dfs(dx, dy, node.tns[u]);
vis[dx][dy] = false;
}
}
}
}

  • 时间复杂度:共有 $m n$ 个起点,每次能往 $4$ 个方向搜索(不考虑重复搜索问题),且搜索的长度不会超过 $10$。整体复杂度为 $O(m n * 4^{10})$
  • 空间复杂度:$O(\sum_{i=0}^{words.length - 1} words[i].length * C)$,$C$ 为字符集大小,固定为 $26$

最后

这是我们「刷穿 LeetCode」系列文章的第 No.212 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!