1. 题目背景与问题描述
今天我们来深入探讨LeetCode第1391题"检查网格中是否存在有效路径"。这是一道中等难度的图遍历问题,考察我们对网格路径搜索和连通性判断的能力。
题目给定一个m x n的网格,每个单元格包含一个1到6的数字,代表不同的街道类型。我们需要判断是否存在一条从网格左上角(0,0)到右下角(m-1,n-1)的有效路径。
街道类型对应的连接方式如下:
- 1:表示连接左单元格和右单元格的街道
- 2:表示连接上单元格和下单元格的街道
- 3:表示连接左单元格和下单元格的街道
- 4:表示连接右单元格和下单元格的街道
- 5:表示连接左单元格和上单元格的街道
- 6:表示连接右单元格和上单元格的街道
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解题思路分析
2.1 问题建模
这道题本质上是一个连通性问题,我们需要判断从起点到终点是否存在一条连通的路径。由于每个单元格的街道类型决定了它与其他单元格的连接方式,我们可以将这个问题建模为图遍历问题。
每个单元格可以看作图中的一个节点,街道类型决定了该节点与哪些相邻节点相连。我们的任务就是从起点节点出发,通过合法的连接方式,能否到达终点节点。
2.2 关键观察点
-
双向连接验证:仅仅从一个单元格能到达下一个单元格是不够的,还必须验证下一个单元格是否也能返回到当前单元格。例如,如果当前单元格是类型3(左下连接),而下方单元格是类型2(上下连接),那么从当前单元格可以向下移动,但从下方单元格无法向上移动回来,这样的连接是无效的。
-
边界条件处理:需要特别注意网格边界的情况,确保不会访问越界的单元格。
-
访问标记:为了避免重复访问和无限循环,需要记录已经访问过的单元格。
2.3 算法选择
对于这类连通性问题,通常有以下几种解决方法:
- 深度优先搜索(DFS):递归或使用栈实现,适合路径探索
- 广度优先搜索(BFS):使用队列实现,适合寻找最短路径
- 并查集(Union-Find):适合处理动态连通性问题
考虑到本题需要判断是否存在路径而非最短路径,DFS和BFS都是合适的选择。本文将重点介绍BFS的实现方式,因为它更直观且易于理解。
3. 详细实现步骤
3.1 预处理连接方向
首先,我们需要为每种街道类型定义其连接方向。我们可以使用一个字典来存储这些信息:
python复制# 定义每种街道类型的连接方向 (dx, dy)
street_directions = {
1: [(0, -1), (0, 1)], # 左右
2: [(-1, 0), (1, 0)], # 上下
3: [(0, -1), (1, 0)], # 左下
4: [(0, 1), (1, 0)], # 右下
5: [(0, -1), (-1, 0)], # 左上
6: [(0, 1), (-1, 0)] # 右上
}
3.2 BFS实现
下面是使用BFS解决问题的完整代码实现:
python复制from collections import deque
def hasValidPath(grid):
if not grid or not grid[0]:
return False
m, n = len(grid), len(grid[0])
visited = [[False for _ in range(n)] for _ in range(m)]
queue = deque()
queue.append((0, 0))
visited[0][0] = True
# 定义每种街道类型的连接方向 (dx, dy)
street_directions = {
1: [(0, -1), (0, 1)], # 左右
2: [(-1, 0), (1, 0)], # 上下
3: [(0, -1), (1, 0)], # 左下
4: [(0, 1), (1, 0)], # 右下
5: [(0, -1), (-1, 0)], # 左上
6: [(0, 1), (-1, 0)] # 右上
}
while queue:
x, y = queue.popleft()
# 到达终点
if x == m - 1 and y == n - 1:
return True
# 获取当前单元格的街道类型
street_type = grid[x][y]
# 遍历所有可能的移动方向
for dx, dy in street_directions[street_type]:
nx, ny = x + dx, y + dy
# 检查新位置是否在网格范围内且未被访问
if 0 <= nx < m and 0 <= ny < n and not visited[nx][ny]:
# 检查新位置的街道是否与当前街道连接
for ndx, ndy in street_directions[grid[nx][ny]]:
if nx + ndx == x and ny + ndy == y:
visited[nx][ny] = True
queue.append((nx, ny))
break
return False
3.3 代码解析
-
初始化:创建访问矩阵和队列,将起点加入队列并标记为已访问。
-
BFS循环:从队列中取出当前单元格,检查是否到达终点。
-
方向处理:根据当前单元格的街道类型,获取可能的移动方向。
-
连接验证:对于每个可能的移动方向,检查新位置是否在网格范围内且未被访问,然后验证新位置的街道是否能连接回当前单元格。
-
终止条件:如果到达终点则返回True,如果队列为空仍未到达则返回False。
4. 复杂度分析与优化
4.1 时间复杂度
BFS的时间复杂度通常是O(V+E),其中V是顶点数,E是边数。在本问题中:
- 顶点数V = m × n
- 每个顶点最多有2条边(因为每种街道类型最多连接2个方向)
因此,总时间复杂度为O(m × n)。
4.2 空间复杂度
空间复杂度主要来自:
- 访问矩阵:O(m × n)
- 队列:最坏情况下O(m × n)
因此,总空间复杂度也是O(m × n)。
4.3 优化思路
-
原地标记:如果允许修改输入网格,可以用特殊值标记已访问的单元格,节省空间。
-
双向BFS:同时从起点和终点开始搜索,当两边的搜索相遇时即找到路径,可以减少搜索空间。
-
提前终止:一旦到达终点立即返回,不需要继续搜索。
5. 常见错误与调试技巧
5.1 常见错误
-
忽略双向连接验证:只检查从当前单元格能否移动到下一个单元格,而没有验证下一个单元格能否连接回当前单元格。
-
边界条件处理不当:在访问相邻单元格时没有检查是否越界。
-
访问标记遗漏:忘记标记已访问的单元格,导致无限循环。
5.2 调试技巧
-
打印中间状态:在BFS循环中打印当前访问的单元格和队列状态,帮助理解算法执行过程。
-
小规模测试用例:使用小的网格(如2x2)手动验证算法是否正确。
-
可视化路径:在找到路径后,可以打印或绘制实际路径,直观验证结果。
6. 实际应用场景
这类网格路径搜索问题在实际中有广泛的应用,例如:
-
城市交通规划:模拟城市街道网络,规划最优路线。
-
电路板布线:在电子设计中,寻找元件之间的连接路径。
-
游戏开发:在迷宫类游戏中寻找玩家或NPC的移动路径。
-
物流配送:在仓库网格中规划拣货路径。
理解这类问题的解决方法,有助于我们在面对实际工程问题时快速找到解决方案。
