1、本次总结的题目通常采用广度优先搜索的方法,由于是求单个源头节点到达目标节点的最小距离,因此,按照广度优先搜索的方法一圈一圈的更新能够达到的位置,然后判断位置中是否有目标节点,每一圈更新能够达到的位置最多只会访问一次
2、常见的题型会设置障碍物、距离更新的规则,稍微复杂点的题目会增加传送门、设置最多可通过几个障碍物等,因此,在一些细节的特殊情况上,需要稍微注意下:源头节点或目标节点本身处在障碍物位置(无法到达)、源头节点和目标节点位置相同(最小距离为0)
3、距离标记可以采用同尺寸的距离矩阵,也可以用类似二叉树求高度的方式用一个简单的变量标记,一圈一圈进行更新
1、单源最小距离问题的基础题目,需要注意细节的特殊情况:源头节点或目标节点本身处在障碍物位置(无法到达)、源头节点和目标节点位置相同(最小距离为0)
2、距离标记可以采用同尺寸的距离矩阵,也可以用类似二叉树求高度的方式用一个简单的变量标记,一圈一圈进行更新
from typing import List, Optional, Union
from collections import deque
'''
lintcode
611 · 骑士的最短路径
给定骑士在棋盘上的 初始 位置(一个二进制矩阵:0表示空1表示有障碍物),找到到达 终点 的最短路线,返回路线的长度。
如果骑士不能到达则返回-1。说明:起点终点必定为空,骑士不能碰到障碍物,路径长度指骑士走的步数。
如果骑士的位置为 (x,y),他下一步可以到达以下这些位置:
(x + 1, y + 2)
(x + 1, y - 2)
(x - 1, y + 2)
(x - 1, y - 2)
(x + 2, y + 1)
(x + 2, y - 1)
(x - 2, y + 1)
(x - 2, y - 1)
示例 1:
输入:grid = [[0,0,0],[0,0,0],[0,0,0]],source = [2,0],target = [2,2]
输出:2
解释:[2,0]->[0,1]->[2,2]
示例 2:
输入:grid = [[0,1,0],[0,0,1],[0,0,0]],source = [2,0],target = [2,2]
输出:-1
题眼:题目其实是求 输入源 距离 目标位置 的最近距离,如果无法到达,则输出-1
思路:单输入源到单目标位置的最短距离(有障碍物):以 输入源/目标位置 为中心进行BFS即可,距离标记可以采用同尺寸的距离矩阵,也可以用类似二叉树求高度的
方式用一个简单的变量标记,一圈一圈进行更新
'''
class Solution:
def shortedPath(self, grid: List[List[int]], source: List[int], target: List[int]) -> int:
# 特殊情况1:起点或终点在障碍物处
if grid[source[0]][source[1]] == 1 or grid[target[0]][target[1]] == 1:
return -1
# 特殊情况2:起点和终点相同
if source[0] == target[0] and source[1] == target[1]:
return 0
m, n = len(grid), len(grid[0])
visited = [[False]*n for _ in range(m)] # 既定义了重复访问,又定义了距离更新
distance = [[0]*n for _ in range(m)] # 定义一个距离矩阵:表示 输入源 +1 扩展 得到新的“输入源”
dq = deque()
dq.append((source[0], source[1]))
visited[source[0]][source[1]] = True
while dq:
x, y = dq.popleft()
# 题目定义的搜索方向如下:
for tx, ty in ((x+1, y+2), (x+1, y-2), (x-1, y+2), (x-1, y-2), (x+2, y+1), (x+2, y-1), (x-2, y+1), (x-2, y-1)):
if 0 <= tx < m and 0 <= ty < n and grid[tx][ty] == 0 and not visited[tx][ty]:
distance[tx][ty] = distance[x][y] + 1
dq.append((tx, ty))
visited[tx][ty] = True
if tx == target[0] and ty == target[1]: # 到达时,返回当前位置的距离
return distance[tx][ty]
return -1
# # BFS求最短路径:用类似二叉树求高度的方式用一个简单的变量标记,一圈一圈进行更新
# result = 0
# m, n = len(grid), len(grid[0])
# visited = [[False] * n for _ in range(m)]
# que = deque()
# que.append((source[0], source[1])) # 每次入队都要判断是否有终点
# visited[source[0]][source[1]] = True
# if source[0] == target[0] and source[1] == target[1]:
# return result
# while len(que) > 0:
# size = len(que)
# result += 1
# for _ in range(size): # 类似二叉树的层序遍历:求二叉树到目标节点的路径长度
# x, y = que.popleft()
# for tx, ty in (
# (x + 1, y + 2), (x + 1, y - 2), (x - 1, y + 2), (x - 1, y - 2), (x + 2, y + 1), (x + 2, y - 1),
# (x - 2, y + 1), (x - 2, y - 1)):
# if 0 <= tx < m and 0 <= ty < n and not visited[tx][ty] and grid[tx][ty] == 0:
# que.append((tx, ty))
# visited[tx][ty] = True
# if tx == target[0] and ty == target[1]:
# return result
# return -1
if __name__ == "__main__":
obj = Solution()
while True:
try:
in_line = input().strip().split('=')
grid = []
for row in in_line[1].strip().split('s')[0][1: -2].split(']')[: -1]:
grid.append([int(n) for n in row.split('[')[1].split(',')])
source = [int(n) for n in in_line[2].split('[')[1].split(']')[0].split(',')]
target = [int(n) for n in in_line[3].split('[')[1].split(']')[0].split(',')]
# print(grid, source, target)
print(obj.shortedPath(grid, source, target))
except EOFError:
break
1、和“611 · 骑士的最短路径”基本是一样的题意, 单源最小距离问题的基础题目,需要注意细节的特殊情况:源头节点或目标节点本身处在障碍物位置(无法到达)、源头节点和目标节点位置相同(最小距离为0)
2、距离标记可以采用同尺寸的距离矩阵,也可以用类似二叉树求高度的方式用一个简单的变量标记,一圈一圈进行更新
from typing import List, Optional, Union
from collections import deque
'''
lintcode
630 · 骑士的最短路径 II
给定骑士在棋盘上的 初始 位置(一个二进制矩阵:0表示空1表示有障碍物),骑士的初始位置是 (0, 0) ,他想要达到 (n - 1, m - 1) 这个位置,
骑士只能从左边走到右边。找到到达 终点 的最短路线,返回路线的长度。
如果骑士不能到达则返回-1。说明:起点终点必定为空,骑士不能碰到障碍物,路径长度指骑士走的步数。
如果骑士的位置为 (x,y),他下一步可以到达以下这些位置:
(x + 1, y + 2)
(x - 1, y + 2)
(x + 2, y + 1)
(x - 2, y + 1)
示例 1:
输入:grid = [[0,0,0,0],[0,0,0,0],[0,0,0,0]]
输出:3
解释:[0,0]->[2,1]->[0,2]->[2,3]
示例 2:
输入:grid = [[0,1,0],[0,0,1],[0,0,0]]
输出:-1
题眼:题目其实是求 输入源 距离 目标位置 的最近距离,如果无法到达,则输出-1
思路:单输入源到单目标位置的最短距离(有障碍物):以 输入源/目标位置 为中心进行BFS即可
'''
class Solution:
def shortedPathII(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
# 特殊情况1:起点或终点在障碍物处
if grid[0][0] == 1 or grid[m-1][n-1] == 1:
return -1
# 特殊情况2:起点和终点相同
if n == 1 and m == 1:
return 0
source, target = [0, 0], [m-1, n-1]
visited = [[False]*n for _ in range(m)] # 既定义了重复访问,又定义了距离更新
distance = [[0]*n for _ in range(m)] # 定义一个距离矩阵:表示 输入源 +1 扩展 得到新的“输入源”
dq = deque()
dq.append((source[0], source[1]))
visited[source[0]][source[1]] = True
while dq:
x, y = dq.popleft()
# 题目定义的搜索方向如下:
for tx, ty in ((x+1, y+2), (x-1, y+2), (x+2, y+1), (x-2, y+1)):
if 0 <= tx < m and 0 <= ty < n and grid[tx][ty] == 0 and not visited[tx][ty]:
distance[tx][ty] = distance[x][y] + 1
dq.append((tx, ty))
visited[tx][ty] = True
if tx == target[0] and ty == target[1]: # 到达时,返回当前位置的距离
return distance[tx][ty]
return -1
if __name__ == "__main__":
obj = Solution()
while True:
try:
in_line = input().strip().split('=')
grid = []
for row in in_line[1].strip().split('s')[0][1: -1].split(']')[: -1]:
grid.append([int(n) for n in row.split('[')[1].split(',')])
print(obj.shortedPathII(grid))
except EOFError:
break
1、和“630 · 骑士的最短路径 II”基本是一样的题意, 单源最小距离问题的基础题目,需要注意细节的特殊情况:源头节点或目标节点本身处在障碍物位置(无法到达)、源头节点和目标节点位置相同(最小距离为0)
2、距离标记可以采用同尺寸的距离矩阵,也可以用类似二叉树求高度的方式用一个简单的变量标记,一圈一圈进行更新
from typing import List, Optional, Union
from collections import deque
'''
1091. 二进制矩阵中的最短路径
给你一个 n x n 的二进制矩阵 grid 中,返回矩阵中最短 畅通路径 的长度。如果不存在这样的路径,返回 -1 。
二进制矩阵中的 畅通路径 是一条从 左上角 单元格(即,(0, 0))到 右下角 单元格(即,(n - 1, n - 1))的路径,该路径同时满足下述要求:
路径途经的所有单元格都的值都是 0 。
路径中所有相邻的单元格应当在 8 个方向之一 上连通(即,相邻两单元之间彼此不同且共享一条边或者一个角)。
畅通路径的长度 是该路径途经的单元格总数。
示例 1:
输入:grid = [[0,1],[1,0]]
输出:2
题眼:题目其实是求 输入源 距离 目标位置 的最近距离,如果无法到达,则输出-1
思路:单输入源到单目标位置的最短距离(有障碍物):以 输入源/目标位置 为中心进行BFS即可
'''
class Solution:
def shortestPathBinaryMatrix(self, grid: List[List[int]]) -> int:
n = len(grid)
# 特殊情况1:起点或者终点单元格值为1
if grid[0][0] == 1 or grid[n-1][n-1] == 1:
return -1
# 特殊情况2:起点和终点位置相同
if n == 1:
return 1
visited = [[False]*n for _ in range(n)] # 既定义了重复访问,又定义了距离更新
distance = [[1]*n for _ in range(n)] # 定义一个距离矩阵:表示 输入源 +1 扩展 得到新的“输入源”:这里是要求单元格数,不是路径长度,因此初始值为1
# 将起点加入队列
dq = deque()
dq.append((0, 0))
visited[0][0] = True
while dq:
x, y = dq.popleft()
# 题目定义的搜索方向如下:四周、对角线
for tx, ty in ((x+1, y), (x-1, y), (x, y+1), (x, y-1), (x+1, y-1), (x-1, y+1), (x+1, y+1), (x-1, y-1)):
if 0 <= tx < n and 0 <= ty < n and grid[tx][ty] == 0 and not visited[tx][ty]:
distance[tx][ty] = distance[x][y] + 1
dq.append((tx, ty))
visited[tx][ty] = True
if tx == n-1 and ty == n-1: # 到达时,返回当前位置的距离
return distance[tx][ty]
return -1
if __name__ == "__main__":
obj = Solution()
while True:
try:
in_line = input().strip().split('=')
grid = []
for row in in_line[1].strip().split('s')[0][1: -1].split(']')[: -1]:
grid.append([int(n) for n in row.split('[')[1].split(',')])
print(obj.shortestPathBinaryMatrix(grid))
except EOFError:
break
1、这道题和上面几道同样是单源最小距离问题的基础题目,但它不需要注意细节的特殊情况了,因为它不是按照位置给定的源头节点或目标节点;该题目需要两次遍历,第一次遍历找到源头节点入队,注意一旦找到就标记访问过并break退出循环,节省运行时间,第二次遍历就和上面几道是同样的过程了
2、距离标记可以采用同尺寸的距离矩阵,也可以用类似二叉树求高度的方式用一个简单的变量标记,一圈一圈进行更新
from typing import List, Optional, Union
from collections import deque
'''
lintcode
750 · 传送门
Chell是Valve Corporation开发的Portal视频游戏系列中的主角。一天,她掉进了一个迷宫。迷宫可以看作是一个大小为n*m二维字符数组。它有4种房间。
'S'代表Chell从哪开始(有且只有一个起点)。'E'代表迷宫的出口(当chell抵达时,她将离开迷宫,该题目可能会有多个出口)。’*'代表这个房间Chell可以经过。
'#'代表-堵墙, Chell不能经过墙。她可以每次上下左右移动到达一一个房间,花费一分钟时间,但是不能到达墙。
现在,你能告诉我她最少需要多少时间离开这个迷宫吗?如果她不能离开,返回-1。
示例 1:
输入:grid = [[‘S’,'E','*'],['*','*','*'],['*','*','*']]
输出:1
解释:[0,0]->[0,1]
示例 2:
输入:grid = [[‘S’,'#','#'],['#','*','#'],['#','*','*'],['#','*','E']]
输出:-1
题眼:题目其实是求 输入源 距离 目标位置 的最近距离,如果无法到达,则输出-1
思路:单输入源到单目标位置的最短距离(有障碍物):以 输入源/目标位置 为中心进行BFS即可
'''
class Solution:
def gateway(self, grid: List[List[str]]) -> int:
m, n = len(grid), len(grid[0])
distance = [[0]*n for _ in range(m)] # 记录能到的路径,距离起点的距离
visited = [[False]*n for _ in range(m)] # 标记访问过的位置,也标记距离更新(起点扩展)
# 第一次遍历:将起点入队
dq = deque()
for i in range(m):
if len(dq) == 1:
break
for j in range(n):
if grid[i][j] == 'S':
dq.append((i, j))
visited[i][j] = True
break
# 第二次遍历:+1扩展得到 新“起点”
while dq:
x, y = dq.popleft()
for tx, ty in ((x+1, y), (x-1, y), (x, y+1), (x, y-1)):
if 0 <= tx < m and 0 <= ty < n and not visited[tx][ty]: # 有效+第一次
if grid[tx][ty] == '*': # 可到达的房间
dq.append((tx, ty))
visited[tx][ty] = True
distance[tx][ty] = distance[x][y] + 1
elif grid[tx][ty] == 'E': # 到达终点,直接返回
return distance[x][y] + 1
return -1 # 无法到达终点
if __name__ == "__main__":
obj = Solution()
while True:
try:
in_line = input().strip().split('=')
grid = []
for row in in_line[1].strip().split('s')[0][1: -1].split(']')[: -1]:
grid.append([n[1: -1] for n in row.split('[')[1].split(',')])
# print(grid)
print(obj.gateway(grid))
except EOFError:
break
1、相比于“750 · 传送门”的题目,这道题增加了一步可到达的传送门,该题目同样也需要两次遍历,第一次遍历找到源头节点入队并标记,同时要建立传送门的哈希表,第二次遍历需要在上面几道题目的基础上,需要额外增加对传送门的判断,并把添加后的哈希表键值删除掉
2、距离标记可以采用同尺寸的距离矩阵,也可以用类似二叉树求高度的方式用一个简单的变量标记,一圈一圈进行更新
from typing import List, Optional, Union
from collections import deque
'''
lintcode
1364 · 最短路径
给出一个二维的表格图,其中每个格子上有一个数字 num。
如果 num 是 -2 表示这个点是起点,num 是 -3 表示这个点是终点,num 是 -1 表示这个点是障碍物不能行走,num 为 0 表示这个点是道路可以正常行走。
如果 num 是正数,表示这个点是传送门,则这个点可以花费 1 的代价到达有着相同数字的传送门格子中。
每次可以花费 1 的代价向上下左右四个方向之一行走一格,传送门格子也可以往四个方向走求出从起点到终点的最小花费,如果不能到达返回 -1。
传送门的种类不会超过50; 即图中最大正数不会超过50。
示例 1:
输入:grid = [[1,0,-1,1],[-2,0,-1,-3],[2,2,0,0]]
输出:3
解释:从-2起点先向上走到1,再通过传送门到达最右上角的1位置 再往下走到达-3终点
题眼:题目其实是求 输入源 距离 目标位置 的最近距离,如果无法到达,则输出-1
思路:单输入源到单目标位置的最短距离(有障碍物、传送门:加哈希表进行处理):以 输入源/目标位置 为中心进行BFS即可
'''
class Solution:
def get_min_distance(self, maze_map: List[List[int]]) -> int:
# 第一次遍历:需要将起点、终点、传送门的位置进行记录
m, n = len(maze_map), len(maze_map[0])
st, ed = (), ()
trans = {}
for i in range(m):
for j in range(n):
if maze_map[i][j] == -2:
st = (i, j)
elif maze_map[i][j] == -3:
ed = (i, j)
elif maze_map[i][j] > 0:
if maze_map[i][j] not in trans:
trans[maze_map[i][j]] = set()
trans[maze_map[i][j]].add((i, j))
else:
trans[maze_map[i][j]].add((i, j))
# 第二次遍历:从起点开始不断 +1 更新距离矩阵
dq = deque()
distance = [[0]*n for _ in range(m)]
visited = [[False]*n for _ in range(m)]
dq.append(st)
visited[st[0]][st[1]] = True
while dq:
x, y = dq.popleft()
for tx, ty in ((x+1, y), (x-1, y), (x, y+1), (x, y-1)):
if 0 <= tx < m and 0 <= ty < n and maze_map[tx][ty] != -1 and not visited[tx][ty]:
dq.append((tx, ty))
visited[tx][ty] = True
distance[tx][ty] = distance[x][y] + 1
if tx == ed[0] and ty == ed[1]:
return distance[tx][ty]
if maze_map[x][y] > 0:
if maze_map[x][y] in trans:
for tx, ty in trans[maze_map[x][y]]:
if not visited[tx][ty]:
dq.append((tx, ty))
visited[tx][ty] = True
distance[tx][ty] = distance[x][y] + 1
trans.pop(maze_map[x][y]) # 避免被重复访问,就把对应的键全部移除
return -1
if __name__ == "__main__":
obj = Solution()
while True:
try:
in_line = input().strip().split('=')[1].strip()[1: -1]
mat = []
for row in in_line.split(']')[: -1]:
mat.append([int(n) for n in row.split('[')[1].split(',')])
print(obj.get_min_distance(mat))
except EOFError:
break
1、这道题设置了最多可通过几个障碍物,因此细节的特殊情况里的源头节点或目标节点本身处在障碍物位置是无法到达就不存在了;同时,即使是求最短路径,某些位置也不是只能被访问一次了,可以被多次访问,因此visited矩阵去重的方式也不适用了;此外,距离标记也不可以采用同尺寸的距离矩阵了,因为对应的位置可能被多次访问而有多个答案了
2、该题不看题解是真的太难了!
from typing import List, Optional, Union
from collections import deque
'''
1293. 网格中的最短路径
给你一个m * n的网格,其中每个单元格不是0(空)就是1(障碍物)。每一步,您都可以在空白单元格中上、下、左、右移动。
如果您 最多 可以消除 k 个障碍物,请找出从左上角 (0, 0) 到右下角 (m-1, n-1) 的最短路径,并返回通过该路径所需的步数。
如果找不到这样的路径,则返回 -1。说明:起点和终点值为0。
示例 1:
输入:grid = [[0,0,0],[1,1,0],[0,0,0],[0,1,1],[0,0,0]], k = 1
输出:6
解释:
不消除任何障碍的最短路径是 10。
消除位置 (3,2) 处的障碍后,最短路径是 6 。该路径是 (0,0) -> (0,1) -> (0,2) -> (1,2) -> (2,2) -> (3,2) -> (4,2).
题眼:题目其实是求 输入源 距离 目标位置 的最近距离,如果无法到达,则输出-1
思路:单输入源到单目标位置的最短距离(有障碍物):以 输入源/目标位置 为中心进行BFS即可
分析:由于玩家在最短路中显然不会经过同一位置超过一次,因此最多消除 k 个障碍物等价于最多经过 k 个障碍物。这样我们就可以使用三元组 (x, y, rest) 表示一个搜索状态。
注意:1、不适合用多源输入到多源输出的模板了(因为位置不是只能访问一次了:虽然答案对,但不好理解)2、是三元组只能访问一次;所以按照 距离+1 一圈一圈进行遍历
'''
class Solution:
def shortestPath(self, grid: List[List[int]], k: int) -> int:
m, n = len(grid), len(grid[0])
# # 特殊情况1:起点或者终点单元格值为1
# if grid[0][0] == 1 or grid[n-1][n-1] == 1:
# return -1
# 特殊情况1:起点和终点位置相同
if m == 1 and n == 1:
return 0
# 特殊情况2:无障碍时,最短路径为m+n-2,经过m+n-3个单元格(除了起点、终点),只要k大于等于m+n-3,最短路径一定是m+n-2
# 能通过60%多的案例
if k >= m+n-3:
return m+n-2
# 接下来的遍历情况都是k < m+n-3
# visited = [[False]*n for _ in range(m)] # 既定义了重复访问,又定义了距离更新
# 在这一题中,定义visited矩阵显然不太行,因为同一个位置可能能被重复访问
visited = set()
distance = 0
# 将起点加入队列
dq = deque()
dq.append((0, 0, k))
visited.add((0, 0, k))
while dq:
size = len(dq)
distance += 1 # 按照距离+1一圈一圈的进行遍历
for _ in range(size):
x, y, rest = dq.popleft()
for tx, ty in ((x+1, y), (x-1, y), (x, y+1), (x, y-1)):
if 0 <= tx < m and 0 <= ty < n:
if grid[tx][ty] == 0 and (tx, ty, rest) not in visited:
dq.append((tx, ty, rest))
visited.add((tx, ty, rest))
if tx == m - 1 and ty == n - 1: # 到达时,返回当前位置的距离:只有在grid==0时才能到达
return distance
elif grid[tx][ty] == 1 and rest > 0 and (tx, ty, rest-1) not in visited:
dq.append((tx, ty, rest-1))
visited.add((tx, ty, rest-1))
return -1
if __name__ == "__main__":
obj = Solution()
while True:
try:
in_line = input().strip().split('=')
grid = []
for row in in_line[1].strip()[1: -4].split(']')[: -1]:
grid.append([int(n) for n in row.split('[')[1].split(',')])
k = int(in_line[2].strip())
print(obj.shortestPath(grid, k))
except EOFError:
break