【专题讲解】搜索专题:BFS,DFS,回溯
BFS
应该注意的是,使用 BFS 只能求解无权图的最短路径,无权图是指从一个节点到另一个节点的代价都记为 1。在程序实现 BFS 时需要考虑以下问题:
队列:用来存储每一轮遍历得到的节点;
标记:对于遍历过的节点,应该将它标记,防止重复遍历。
例题 单词接龙
一道很有意思的题目,思想较为简单,每次查找是否存在一个只有一个不同的字符的单词。如果有存入队列,并且查找邻居。
有一个难点在于,如何查找到只有一个不同的字符的单词。题解给出了通配符的方法。值得学习。
另外就是可以通过两端同时bfs加速搜索。从终点和起点同时开始搜索,维护两个字典标记已经查找到的邻居和查找到该邻居时对应的查找次数。
class Solution(object):
#### 双向bfs
def __init__(self):
self.length = 0
self.all_combo_dict = collections.defaultdict(list)
def visitWordNode(self, queue, visited, others_visited):
current_word, level = queue.popleft()
for i in range(self.length):
intermediate_word = current_word[:i] + "*" + current_word[i+1:]
for word in self.all_combo_dict[intermediate_word]:
if word in others_visited:
return level + others_visited[word]
if word not in visited:
visited[word] = level + 1
queue.append((word, level + 1))
return None
def ladderLength(self, beginWord, endWord, wordList):
if endWord not in wordList or not endWord or not beginWord or not wordList:
return 0
self.length = len(beginWord)
for word in wordList:
for i in range(self.length):
self.all_combo_dict[word[:i] + "*" + word[i+1:]].append(word)
queue_begin = collections.deque()
queue_end = collections.deque()
queue_begin.append((beginWord, 1))
queue_end.append((endWord, 1))
visited_begin = {beginWord: 1}
visited_end = {endWord: 1}
ans = None
while queue_begin and queue_end:
ans = self.visitWordNode(queue_begin, visited_begin, visited_end)
if ans:
return ans
ans = self.visitWordNode(queue_end, visited_end, visited_begin)
if ans:
return ans
return 0
DFS
一般采用递归的思想解决DFS,要注意标记每次DFS已经经过的路径。
这道题目虽然时DFS的题目,但是解决策略还是存在很大的说法的。
我在解决这道题目时首先想到是采用每个点进行DFS的策略,查找是否与大西洋或太平洋接壤。
class Solution:
# 采用逆流的思路更快
def pacificAtlantic(self, matrix: List[List[int]]) -> List[List[int]]:
if not matrix:
return matrix
n = len(matrix)
m = len(matrix[0])
ans = []
move = [[0,1],[1,0],[-1,0],[0,-1]]
#visited = set()
def dfs(index,visited):
x1,y1 = index
a,b = False,False
if x1 == 0 or y1 == 0:
a = True
if x1 == n-1 or y1 == m-1:
b = True
if a and b:
#print(a,b)
return [a,b]
for xtemp, ytemp in move:
x, y = x1+xtemp, y1+ytemp
if 0<=x<n and 0<=y<m and matrix[x1][y1]>=matrix[x][y] and not (x,y) in visited:
visited.add((x,y))
# !!!如果要返回两个量一定要先提取出来这两个量。
P,A = dfs([x,y],visited)
a = a or P
b = b or A
if a and b:
return [a,b]
return [a,b]
for i in range(n):
for j in range(m):
# !! 这个地方注意,每次dfs都要取消之前dfs的标记
visited = set()
visited.add((i,j))
a,b = dfs([i,j],visited)
#print(a,b)
if a and b:
ans.append([i,j])
return ans
这种做法有一些需要注意的地方
- 每次dfs的标记都是不同的,开始新的一次dfs要记着首先重置之前的标记
- 当函数返回两个值,一定要值调用一次函数,一次性返回两个值,而不是两次调用函数。
另外这道题目如果采用河流逆流的方式会更好,也就是首先标记整个区域中,太平洋可以“倒灌”到哪里。在同样对大西洋进行操作,最后取两者的交集。
class Solution(object):
def __init__(self):
self.directs = [(-1, 0), (0, 1), (1, 0), (0, -1)]
def pacificAtlantic(self, matrix) :
if not matrix:
return []
n = len(matrix)
m = len(matrix[0])
self.ao = [[0] * m for _ in range(n)]
self.po = [[0] * m for _ in range(n)]
self.visited = [[0] * m for _ in range(n)]
for j in range(m): # 从上面的太平洋逆流
self.dfs(matrix, 0, j, True)
for i in range(n): # 从左边的太平洋逆流
self.dfs(matrix, i, 0, True)
self.visited = [[0] * m for _ in range(n)]
for j in range(m): # 下面的大西洋
self.dfs(matrix, n-1, j, False)
for i in range(n): # 右边的大西洋
self.dfs(matrix, i, m-1, False)
result_all = []
for i in range(n):
for j in range(m):
if self.po[i][j] == 1 and self.ao[i][j] == 1:
result_all.append([i, j])
return result_all
def dfs(self, matrix, x, y, flag):
if self.visited[x][y] == 1:
return
self.visited[x][y] = 1
n = len(matrix)
m = len(matrix[0])
if flag: # 表示是太平洋
self.po[x][y] = 1
else: # 表示是大西洋
self.ao[x][y] = 1
for i in range(4):
newx = x + self.directs[i][0]
newy = y + self.directs[i][1]
if 0 <= newy < m and 0 <= newx < n and matrix[x][y] <= matrix[newx][newy]:
self.dfs(matrix, newx, newy, flag)
return
回溯算法
Backtracking(回溯)属于 DFS。
普通 DFS 主要用在 可达性问题 ,这种问题只需要执行到特点的位置然后返回即可。
而 Backtracking 主要用于求解 排列组合 问题,例如有 { ‘a’,‘b’,‘c’ } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。
因为 Backtracking 不是立即返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题:
- 在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素;
- 但是在递归返回时,需要将元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素。
基本框架:
- 判断是否完成任务:
- 进入循环,首先判断是否满足
- 如果满足,加入当前目标,进入递归
- 结束递归之后,删除当前目标
例题 二叉树的所有路径
可以回溯的解决,并且相当于直观的展示出回溯算法的所有结果。
- 回溯结束的标志,左右节点都空
- 先探索左侧节点,在探索右侧节点。
class Solution:
def binaryTreePaths(self, root: TreeNode) -> List[str]:
ans = []
if not root:
return ans
res = ''
def backtraking(root,res):
if not root.left and not root.right:
ans.append(res+str(root.val))
if root.left:
backtraking(root.left, res+str(root.val)+'->')
if root.right:
backtraking(root.right, res+str(root.val)+'->')
return
backtraking(root, res)
return ans
# 迭代方法
class Solution:
def binaryTreePaths(self, root):
if not root:
return []
paths = []
stack = [(root, str(root.val))]
while stack:
node, path = stack.pop()
if not node.left and not node.right:
paths.append(path)
if node.left:
stack.append((node.left, path + '->' + str(node.left.val)))
if node.right:
stack.append((node.right, path + '->' + str(node.right.val)))
return paths
注意这里的迭代方法,每次存储了节点的信息。
例题2 含有重复元素的组合
上一篇: leetcode水题第三周
下一篇: Tire树