212. 单词搜索 II - 力扣(LeetCode)
一、题目
给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words, 返回所有二维网格上的单词 。
单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。
示例 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"] 输出:[]
提示:
- m == board.length
- n == board[i].length
- 1 <= m, n <= 12
- board[i][j] 是一个小写英文字母
- 1 <= words.length <= 3 * 104
- 1 <= words[i].length <= 10
- words[i] 由小写英文字母组成
- words 中的所有字符串互不相同
二、代码
class Solution {
public List<String> findWords(char[][] board, String[] words) {
// 记录前缀树中的所有字符串,做去重
HashSet<String> trieSet = new HashSet<>();
// 前缀树的根节点
TrieNode head = new TrieNode();
// 利用单词表构造前缀树
for (String word : words) {
// 相同的单词就去重
if (!trieSet.contains(word)) {
addTrieNode(head, word);
trieSet.add(word);
}
}
// 记录递归轨迹中走过的字符
LinkedList<Character> path = new LinkedList<>();
// 要返回的答案
List<String> ans = new ArrayList<>();
// 尝试以每一个位置作为起始点,看看能不能找到一个单词能和前缀树中的单词匹配上
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
process(board, i, j, head, path, ans);
}
}
return ans;
}
public int process(char[][] board, int row, int col, TrieNode node, LinkedList<Character> path, List<String> ans) {
// 如果矩阵中该位置为0,说明该位置已经走过了,不要重复走了,直接返回0
if (board[row][col] == 0 ) {
return 0;
}
char cha = board[row][col];
// 如果当前矩阵中的字符在前缀树中没有相应的路线,也返回0
if (node.next[cha - 'a'] == null || node.next[cha - 'a'].pass == 0) {
return 0;
}
TrieNode next = node.next[cha - 'a'];
board[row][col] = 0;
path.addLast(cha);
int cnt = 0;
// 如果来到了一个单词的结尾字符,就说明找到了一个单词,将该单词加入到ans中
if (next.end == true) {
ans.add(charListToString(path));
next.end = false;
cnt++;
}
// 开始尝试四个方向,并且需要保证不越界
if (row + 1 < board.length) {
//node.pass--;
cnt += process(board, row + 1, col, next, path, ans);
}
if (row - 1 >= 0) {
//node.pass--;
cnt += process(board, row - 1, col, next, path, ans);
}
if (col + 1 < board[0].length) {
//node.pass--;
cnt += process(board, row, col + 1, next, path, ans);
}
if (col - 1 >= 0) {
//node.pass--;
cnt += process(board, row, col - 1, next, path, ans);
}
// 恢复现场
board[row][col] = cha;
path.pollLast();
next.pass -= cnt;
return cnt;
}
// 将字符List转换为String
public String charListToString(LinkedList<Character> path) {
StringBuilder sb = new StringBuilder();
for (Character c : path) {
sb.append(c);
}
return sb.toString();
}
// 前缀树节点类
class TrieNode {
public TrieNode[] next;
// 记录该节点被不同单词通过的次数
public int pass;
// 该节点是否为单词结束位置
public boolean end;
public TrieNode() {
next = new TrieNode[26];
pass = 0;
end = false;
}
}
// 将word加入前缀树
public void addTrieNode(TrieNode head, String word) {
char[] w = word.toCharArray();
TrieNode node = head;
node.pass++;
for (int i = 0; i < w.length; i++) {
if (node.next[w[i] - 'a'] == null) {
node.next[w[i] - 'a'] = new TrieNode();
}
// node向下移动一个位置
node = node.next[w[i] - 'a'];
// 将下面的node的pass也加1
node.pass++;
}
// 标记单词结尾节点
node.end = true;
}
}
三、解题思路
尝试以矩阵中每个点作为出发点,收集单词。先将单词表中的所有单词建成前缀树,这样可以加快匹配速度。只要是涉及到字符串匹配的,马上要想到可以利用前缀树优化。
来到某一个(i,j)位置的字符a,先看前缀树的头节点的直接子路线有没有a,发现有a,那么就说明可以从a开始找。如果没有a就直接跳过,去尝试以下一个位置的字符作为开始点查找。
然后从a开始可以往上下左右4个方向走,至于到底有没有必要往某个方向走,也可以用前缀树来指导。例如我们如果发现a的直接子路线没有a在矩阵中上下左右的字符,那么就没有方向可以走。