题目描述:
给定一个 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”]
输出:[]
思路:
相当于在原来79问题的基础上,变成了检查一串字符串,为了避免重复的遍历board,将所给出的words构建一个字典树,然后从board的第一个字母开始,按照移动方式是否会在字典树中,这里用到一个技巧就是利用两个数组
self.dx = [-1, +1, 0, 0] # 用来控制上下左右
self.dy = [0, 0, -1, +1]
class Solution:
def __init__(self):
self.root = {}
self.end = '#'
self.dx = [-1, +1, 0, 0] # 用来控制上下左右
self.dy = [0, 0, -1, +1]
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
# 以候选池中的单词构成一个字典树,然后从board的每一个元素开始搜索,要是有在trie上的就加入到结果中
if len(board) == 0 and len(board[0]) == 0:
return []
if len(words) == 0:
return []
self.result = []
# 利用候选池中的单词构成一个字典树
for i in range(len(words)):
self.insert(words[i])
# 进行查找满足的,注意需要进行判重,所以需要一个额外的数组
visited = []
for a in range(len(board)):
visited.append([])
for b in range(len(board[0])):
visited[a].append(0) # 0表示没有访问过
# 查找
for i in range(len(board)):
for j in range(len(board[0])):
if board[i][j] in self.root.keys():
self.dfs(i, j, board, visited, "", self.root)
# 将self.result进行字典序排序
# self.result.sort()
self.res = []
for i in range(len(self.result)):
if self.result[i] not in self.res:
self.res.append(self.result[i])
return self.res
def insert(self, word):
if not word:
return
node = self.root
for char in word:
if char in node.keys():
node = node[char]
else:
node[char] = {}
node = node[char]
node[self.end] = self.end
def dfs(self, i, j, board, visited, cur_words, cur_dict):
cur_words = cur_words + board[i][j]
visited[i][j] = 1
cur_dict = cur_dict[board[i][j]]
# print(cur_dict)
if self.end in cur_dict:
self.result.append(cur_words)
for yi in range(4):
x = i + self.dx[yi]
y = j + self.dy[yi]
if 0 <= x < len(board) and 0 <= y < len(board[0]) and \
visited[x][y] != 1 and board[x][y] in cur_dict:
self.dfs(x, y, board, visited, cur_words, cur_dict)
visited[i][j] = 0
官方的方法:
(一)与上述的整体思路是一致的,只是实现的方式不一样。
根据题意,我们需要逐个遍历二维网格中的每一个单元格;然后搜索从该单元格出发的所有路径,找到其中对应 \textit{words}words 中的单词的路径。因为这是一个回溯的过程,所以我们有如下算法:
遍历二维网格中的所有单元格。
深度优先搜索所有从当前正在遍历的单元格出发的、由相邻且不重复的单元格组成的路径。因为题目要求同一个单元格内的字母在一个单词中不能被重复使用;所以我们在深度优先搜索的过程中,每经过一个单元格,都将该单元格的字母临时修改为特殊字符(例如 #),以避免再次经过该单元格。
如果当前路径是words 中的单词,则将其添加到结果集中。如果当前路径是 words 中任意一个单词的前缀,则继续搜索;反之,如果当前路径不是 words 中任意一个单词的前缀,则剪枝。我们可以将words 中的所有字符串先添加到前缀树中,而后用 O(|S|)O(∣S∣) 的时间复杂度查询当前路径是否为words 中任意一个单词的前缀。
在具体实现中,我们需要注意如下情况:
因为同一个单词可能在多个不同的路径中出现,所以我们需要使用哈希集合对结果集去重。
在回溯的过程中,我们不需要每一步都判断完整的当前路径是否是 words 中任意一个单词的前缀;而是可以记录下路径中每个单元格所对应的前缀树结点,每次只需要判断新增单元格的字母是否是上一个单元格对应前缀树结点的子结点即可。
from collections import defaultdict
class Trie:
def __init__(self):
self.children = defaultdict(Trie)
self.word = ""
def insert(self, word):
cur = self
for c in word:
cur = cur.children[c]
cur.is_word = True
cur.word = word
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
trie = Trie()
for word in words:
trie.insert(word)
def dfs(now, i1, j1):
if board[i1][j1] not in now.children:
return
ch = board[i1][j1]
now = now.children[ch]
if now.word != "":
ans.add(now.word)
board[i1][j1] = "#"
for i2, j2 in [(i1 + 1, j1), (i1 - 1, j1), (i1, j1 + 1), (i1, j1 - 1)]:
if 0 <= i2 < m and 0 <= j2 < n:
dfs(now, i2, j2)
board[i1][j1] = ch
ans = set()
m, n = len(board), len(board[0])
for i in range(m):
for j in range(n):
dfs(trie, i, j)
return list(ans)
(二)当我们使用方法一来找出所有同时在二维网格和单词列表中出现的单词时,我们需要遍历每一个单元格的所有路径,会找到大量重复的单词。
为了缓解这种情况,我们可以将匹配到的单词从前缀树中移除,来避免重复寻找相同的单词。因为这种方法可以保证每个单词只能被匹配一次;所以我们也不需要再对结果集去重了。
from collections import defaultdict
class Trie:
def __init__(self):
self.children = defaultdict(Trie)
self.word = ""
def insert(self, word):
cur = self
for c in word:
cur = cur.children[c]
cur.is_word = True
cur.word = word
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
trie = Trie()
for word in words:
trie.insert(word)
def dfs(now, i1, j1):
if board[i1][j1] not in now.children:
return
ch = board[i1][j1]
nxt = now.children[ch]
if nxt.word != "":
ans.append(nxt.word)
nxt.word = ""
if nxt.children:
board[i1][j1] = "#"
for i2, j2 in [(i1 + 1, j1), (i1 - 1, j1), (i1, j1 + 1), (i1, j1 - 1)]:
if 0 <= i2 < m and 0 <= j2 < n:
dfs(nxt, i2, j2)
board[i1][j1] = ch
if not nxt.children:
now.children.pop(ch)
ans = []
m, n = len(board), len(board[0])
for i in range(m):
for j in range(n):
dfs(trie, i, j)
return ans
复杂度分析
时间复杂度:O(m×n×3^
l−1),其中 mm 是二维网格的高度,n 是二维网格的宽度,l是最长单词的长度。我们仍需要遍历 m×n 个单元格,每个单元格在最坏情况下仍需要遍历4×3 ^l−1条路径。
空间复杂度:O(k×l),其中 k 是 words 的长度,l 是最长单词的长度。最坏情况下,我们需要 O(k×l) 用于存储前缀树。