1. 题目解析与需求拆解
LeetCode 980题"不同的路径III"是一道典型的网格回溯问题。题目给定一个二维整数矩阵grid,其中:
- 0代表空方格可以行走
- 1代表起点
- 2代表终点
- -1代表障碍物不可通过
我们需要找到从起点到终点并经过所有空方格的唯一路径数。这个题目在算法面试中非常典型,考察的是对DFS+回溯算法的掌握程度。
1.1 问题特征分析
这道题有几个关键特征需要注意:
- 必须经过所有可通行格子:不像普通路径问题只需要到达终点,这里要求路径必须覆盖所有0格
- 网格规模有限:题目提示grid.length和grid[i].length都不超过20
- 四种格子状态:需要正确处理起点、终点、障碍物和空格的逻辑关系
1.2 解题思路选择
面对这种需要探索所有可能路径的问题,DFS(深度优先搜索)是最自然的选择。因为:
- 需要穷举所有可能的行走路线
- 网格规模有限(20x20),DFS的时间复杂度可以接受
- 需要记录已访问的格子状态,回溯算法能很好地处理这种需求
2. 算法设计与实现细节
2.1 基础DFS框架
我们先构建DFS的基本框架:
public int uniquePathsIII(int[][] grid) { // 初始化必要的变量 int startX = 0, startY = 0; int empty = 0; // 预处理:找到起点和统计需要经过的空格数 for(int i = 0; i < grid.length; i++) { for(int j = 0; j < grid[0].length; j++) { if(grid[i][j] == 1) { startX = i; startY = j; } if(grid[i][j] == 0) { empty++; } } } return dfs(grid, startX, startY, empty); } private int dfs(int[][] grid, int x, int y, int empty) { // 边界检查和障碍物检查 if(x < 0 || x >= grid.length || y < 0 || y >= grid[0].length || grid[x][y] == -1) { return 0; } // 到达终点时的判断 if(grid[x][y] == 2) { return empty == -1 ? 1 : 0; } // 标记当前格子为已访问 grid[x][y] = -1; empty--; // 四个方向探索 int paths = dfs(grid, x+1, y, empty) + dfs(grid, x-1, y, empty) + dfs(grid, x, y+1, empty) + dfs(grid, x, y-1, empty); // 回溯,恢复格子状态 grid[x][y] = 0; empty++; return paths; }2.2 关键优化点
在实际编码中,有几个关键点需要注意优化:
空格计数处理:empty变量初始值为所有0格的数量,每次进入一个0格就减1,到达终点时检查是否empty==-1(因为终点不算在empty计数中)
访问标记与回溯:使用原地修改grid值为-1来标记已访问,回溯时需要恢复为原值
提前终止条件:可以在递归前检查剩余empty是否为负,提前终止无效路径
2.3 复杂度分析
- 时间复杂度:最坏情况下是O(4^(m*n)),因为每个格子有4个方向选择
- 空间复杂度:O(m*n)来自递归栈的深度
虽然理论复杂度很高,但实际由于存在大量提前终止条件(遇到障碍、边界、已访问格子),实际运行时间是可以接受的。
3. 完整实现与测试用例
3.1 完整Java实现
class Solution { public int uniquePathsIII(int[][] grid) { int startX = 0, startY = 0; int empty = 1; // 初始化为1,把起点也算作需要"访问"的格子 for(int i = 0; i < grid.length; i++) { for(int j = 0; j < grid[0].length; j++) { if(grid[i][j] == 1) { startX = i; startY = j; } else if(grid[i][j] == 0) { empty++; } } } return dfs(grid, startX, startY, empty); } private int dfs(int[][] grid, int x, int y, int empty) { if(x < 0 || x >= grid.length || y < 0 || y >= grid[0].length || grid[x][y] < 0) { return 0; } if(grid[x][y] == 2) { return empty == 0 ? 1 : 0; } grid[x][y] = -2; // 标记为已访问 empty--; int paths = dfs(grid, x+1, y, empty) + dfs(grid, x-1, y, empty) + dfs(grid, x, y+1, empty) + dfs(grid, x, y-1, empty); grid[x][y] = 0; // 回溯 empty++; return paths; } }3.2 测试用例验证
public static void main(String[] args) { Solution solution = new Solution(); // 测试用例1 int[][] grid1 = {{1,0,0,0},{0,0,0,0},{0,0,2,-1}}; System.out.println(solution.uniquePathsIII(grid1)); // 输出2 // 测试用例2 int[][] grid2 = {{1,0,0,0},{0,0,0,0},{0,0,0,2}}; System.out.println(solution.uniquePathsIII(grid2)); // 输出4 // 测试用例3 int[][] grid3 = {{0,1},{2,0}}; System.out.println(solution.uniquePathsIII(grid3)); // 输出0 }4. 常见问题与调试技巧
4.1 常见错误排查
无限递归问题:
- 现象:程序卡死或栈溢出
- 原因:忘记标记已访问的格子,导致在两个格子间来回走
- 解决:确保在进入格子后立即标记为已访问
路径计数错误:
- 现象:返回的路径数比预期多或少
- 原因:empty计数逻辑错误或终点判断条件不正确
- 解决:仔细检查empty的初始值和变化逻辑
回溯状态恢复错误:
- 现象:第二次运行得到错误结果
- 原因:没有正确恢复grid和empty的状态
- 解决:确保在递归返回后恢复所有修改的状态
4.2 调试技巧
打印递归路径:
- 在dfs方法开始时打印当前坐标和empty值
- 可以帮助理解递归的走向和状态变化
可视化小网格:
- 对于2x2或3x3的小网格,手动绘制所有可能路径
- 与程序输出对比验证
边界条件测试:
- 专门测试只有起点和终点的最小网格
- 测试障碍物完全包围起点的情况
4.3 性能优化建议
提前终止无效路径:
- 在递归前检查剩余empty是否足够走到终点
- 可以计算当前点到终点的曼哈顿距离
记忆化搜索:
- 虽然难以直接应用,但可以考虑缓存某些中间状态
- 对于大规模重复子问题可能有效
迭代式DFS:
- 使用栈代替递归可以避免栈溢出
- 实现更复杂但可以处理更大网格
5. 算法扩展与变种
5.1 类似题目推荐
LeetCode 63.不同路径II:
- 更简单的网格路径问题
- 使用动态规划解法更高效
LeetCode 212.单词搜索II:
- 结合Trie树的网格DFS问题
- 练习更复杂的终止条件
LeetCode 37.解数独:
- 经典的回溯算法应用
- 需要处理更复杂的约束条件
5.2 问题变种思考
允许重复经过某些格子:
- 修改访问标记逻辑
- 可能需要设置最大重复次数
寻找最短路径:
- 结合BFS和优先级队列
- 需要记录路径长度
三维网格路径:
- 扩展到三维空间
- 递归方向增加到6个
5.3 实际应用场景
这种网格路径问题在实际中有很多应用:
- 机器人路径规划
- 游戏中的AI移动逻辑
- 电路板布线算法
- 物流仓储中的货物拣选路径优化
理解这类问题的解法可以帮助我们解决许多现实中的路径搜索和规划问题。