这是 LeetCode 上的「212. 单词搜索 II」,难度为「困难」。
Tag : 「回溯算法」、「DFS」、「字典树」
给定一个
二维字符网格
和一个单词(字符串)列表
,找出所有同时在二维网格和字典中出现的单词。
单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。
示例 1:
输入: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:
输入:board = [["a","b"],["c","d"]], words = ["abcb"]
输出:[]
提示:
数据范围只有
,且 words
中出现的单词长度不会超过
,可以考虑使用「回溯算法」。
起始先将所有 words
出现的单词放到 Set
结构中,然后以 board
中的每个点作为起点进行爆搜(由于题目规定在一个单词中每个格子只能被使用一次,因此还需要一个 vis
数组来记录访问过的位置):
,直接剪枝;
Set
中,则添加到答案(同时了防止下一次再搜索到该字符串,需要将该字符串从 Set
中移除)。代码:
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);
}
}
}
个起点,每次能往
个方向搜索(不考虑重复搜索问题),且搜索的长度不会超过
。整体复杂度为
在「解法一」中,对于任意一个当前位置
,我们都不可避免的搜索了四联通的全部方向,这导致了那些无效搜索路径最终只有长度达到
才会被剪枝。
要进一步优化我们的搜索过程,需要考虑如何在每一步的搜索中进行剪枝。
我们可以使用
结构进行建树,对于任意一个当前位置
而言,只有在
中存在往从字符
到
的边时,我们才在棋盘上搜索从
到
的相邻路径。
不了解
的同学,可以看看这篇题解 (题解) 208. 实现 Trie (前缀树),里面写了两种实现
的方式。
对于本题,我们可以使用「TrieNode」的方式进行建
。
因为 words
里最多有
个单词,每个单词长度最多为
,如果开成静态数组的话,不考虑共用行的问题,我们需要开一个大小为
的大数组,可能会有 TLE 或 MLE 的风险。
与此同时,我们需要将平时建
中的 isEnd
标记属性直接换成记录当前字符 s
,这样我们在 DFS
的过程中则无须额外记录当前搜索字符串。
代码:
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;
}
}
}
}
个起点,每次能往
个方向搜索(不考虑重复搜索问题),且搜索的长度不会超过
。整体复杂度为
,
为字符集大小,固定为
这是我们「刷穿 LeetCode」系列文章的第 No.212
篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。
在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。
为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode。
在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。