LeetCode 面试题 08.02. 迷路的机器人
题目描述

题意分析
网格中
0表示可走、1表示障碍。机器人从左上角出发,只能向下或向右移动,返回一条按经过顺序记录坐标的完整路径,包含起点和右下角终点;若无法到达,返回空列表。
解法:回溯加失败位置剪枝
核心思路
[!blue]
令
dfs(row, col)表示尝试从当前格继续走到终点。进入一个合法格子后,把坐标加入path,此时path恰好记录从起点走到当前格的路线。接着先向下、再向右搜索:任一方向成功,就保留路径并向上返回成功;两个方向都失败,才移除当前坐标,恢复进入本层前的路径。同一个格子可能由上方和左方的不同路线到达,但它后面能否到达终点,只取决于这个格子以及右下方的障碍,与此前怎么走来无关。因此一个格子确认走不通之后,其他路线再到这里也不必重复搜索,可以保留访问标记剪枝。
代码在进入格子时就将
visited置为true,而不是等失败后才标记,这仍然正确。每次移动都会让row + col增大,所以不可能回到当前递归路径上的祖先。以后再次遇到已访问格子时,它只能来自一个已经结束的旧分支;若那个分支成功,成功结果早已沿调用链返回,整个搜索也会停止。因此搜索仍在继续就说明旧分支已经失败,可以直接剪掉。
path与visited的用途不同:前者只保留当前这条候选路线,失败时必须回退;后者用于避免重复展开已经搜索过的位置,不能随路径一起撤销。这样每个可走格子最多展开一次。越界、障碍、已访问位置直接返回失败,且不加入路径。终点则在入路径后立即返回成功,保证结果包含终点。若起点或终点是障碍,就找不到路径;网格只有一个格子且它可走时,加入起点后也就已经到达终点。
解题步骤
- 空网格直接返回空列表,否则建立与网格同尺寸的
visited,从(0, 0)开始搜索。- 进入递归先检查当前位置;只要越界、遇到障碍或已经访问,就返回
false。- 将当前位置加入
path并标记访问;若已经到达右下角,返回true。- 依次搜索下方与右方,任一成功就立即返回
true,不撤销已找到的路径。- 两个方向都失败,移除本层加入的坐标并返回
false。起点搜索成功时返回保留的路径,否则返回空列表。
代码实现
class Solution {
public List<List<Integer>> pathWithObstacles(int[][] obstacleGrid) {
List<List<Integer>> path = new ArrayList<>();
if (obstacleGrid == null || obstacleGrid.length == 0 || obstacleGrid[0].length == 0) {
return path;
}
boolean[][] visited = new boolean[obstacleGrid.length][obstacleGrid[0].length];
if (dfs(obstacleGrid, 0, 0, visited, path)) {
return path;
}
return new ArrayList<>();
}
private boolean dfs(
int[][] grid, int row, int col, boolean[][] visited, List<List<Integer>> path) {
if (row >= grid.length
|| col >= grid[0].length
|| grid[row][col] == 1
|| visited[row][col]) {
return false;
}
path.add(Arrays.asList(row, col));
// 先标记已进入;后续分支重遇时,此处此前必已搜索失败
visited[row][col] = true;
if (row == grid.length - 1 && col == grid[0].length - 1) {
return true;
}
if (dfs(grid, row + 1, col, visited, path) || dfs(grid, row, col + 1, visited, path)) {
return true;
}
// 两方向都失败才撤销路径;成功时保留完整答案
path.remove(path.size() - 1);
return false;
}
}
func pathWithObstacles(obstacleGrid [][]int) [][]int {
path := [][]int{}
if len(obstacleGrid) == 0 || len(obstacleGrid[0]) == 0 {
return path
}
rows, cols := len(obstacleGrid), len(obstacleGrid[0])
visited := make([][]bool, rows)
for i := 0; i < rows; i++ {
visited[i] = make([]bool, cols)
}
var dfs func(int, int) bool
dfs = func(row int, col int) bool {
if row >= rows || col >= cols || obstacleGrid[row][col] == 1 || visited[row][col] {
return false
}
path = append(path, []int{
row,
col,
})
// 先标记已进入;后续分支重遇时,此处此前必已搜索失败
visited[row][col] = true
if row == rows-1 && col == cols-1 {
return true
}
if dfs(row+1, col) || dfs(row, col+1) {
return true
}
// 两方向都失败才撤销路径;成功时保留完整答案
path = path[:len(path)-1]
return false
}
if dfs(0, 0) {
return path
}
return [][]int{}
}
复杂度分析
- 时间复杂度:$O(mn)$,其中
m、n是网格的行数与列数。每个可走格子最多展开一次,每次只尝试两个方向;重复访问或无效位置会立即返回。- 空间复杂度:$O(mn)$。访问表占 $O(mn)$,当前路径和递归栈最长为 $O(m+n)$,因为只能向右或向下。
关键点总结
[!green]
- 路径入栈与失败回退成对出现,成功时沿调用链保留完整结果。
- 无环的移动规则与成功后的立即返回,保证后续重遇已访问位置时可以按失败处理。
- 每个位置后面的可达性与来路无关,保留访问标记才能消除重复搜索。
易错点总结
[!yellow]
- 无论成功失败都弹出坐标,会把已经找到的答案清空;失败时不弹出,又会把死路混进结果。
- 把
visited当作当前路径标记并在回溯时清除,会反复搜索同一个失败位置。- 必须先排除障碍,再判断终点,不能把有障碍的右下角当成合法终点。
- 下方成功后要立即停止;继续探索并修改同一个
path会破坏已经得到的结果。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 63. 不同路径 II | 中等 | 移动和障碍规则相近,原题统计路径条数,本题只需返回一条可行路径。 |
| 64. 最小路径和 | 中等 | 原题还最小化路径代价,本题没有权值优化,只要记录一条可达前驱链。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!