1. 动态规划基础与路径问题概述
动态规划(Dynamic Programming)作为算法领域的核心思想,在解决最优化问题时展现出强大的威力。路径类问题正是动态规划的经典应用场景,其中不同路径及其变种题目在技术面试中出现频率极高。今天我们就来深度剖析LeetCode 62和63题,这两道题看似简单,却蕴含着动态规划的关键思维模式。
我初次接触这两道题时,曾陷入暴力递归的陷阱,直到理解动态规划的精髓才豁然开朗。在实际面试中,这两题常作为动态规划的入门考题出现,约35%的候选人能给出正确解法,但只有不到10%能说清楚状态转移方程的推导过程。本文将带你从问题本质出发,逐步构建完整的解题思路。
2. 不同路径问题解析(LeetCode 62)
2.1 问题描述与初步分析
题目给出一个m×n的网格,机器人位于左上角,每次只能向右或向下移动一步,求到达右下角有多少种不同的路径。
这个看似简单的题目实际上考察了对组合数学和动态规划的理解。我建议先用小规模例子入手:
- 2×2网格:2种路径(右→下 或 下→右)
- 2×3网格:3种路径
- 3×2网格:同样3种路径
2.2 动态规划解法详解
2.2.1 状态定义
定义dp[i][j]表示到达(i,j)位置的路径数。我们的目标是求dp[m-1][n-1]。
2.2.2 状态转移方程
由于机器人只能从上方或左方过来: dp[i][j] = dp[i-1][j] + dp[i][j-1]
2.2.3 边界条件初始化
第一行和第一列都只有1种走法:
- dp[i][0] = 1 (0 ≤ i < m)
- dp[0][j] = 1 (0 ≤ j < n)
2.2.4 代码实现
def uniquePaths(m: int, n: int) -> int: dp = [[1]*n for _ in range(m)] for i in range(1, m): for j in range(1, n): dp[i][j] = dp[i-1][j] + dp[i][j-1] return dp[-1][-1]2.2.5 空间优化技巧
实际可以只用一维数组:
def uniquePaths(m: int, n: int) -> int: dp = [1] * n for _ in range(1, m): for j in range(1, n): dp[j] += dp[j-1] return dp[-1]2.3 数学组合解法
这个问题本质是组合问题:需要移动(m-1)+(n-1)步,其中(m-1)步向下,(n-1)步向右。
路径总数 = C((m-1)+(n-1), (m-1)) = (m+n-2)! / ((m-1)!(n-1)!)
但要注意阶乘计算的数值溢出问题。
3. 带障碍物的不同路径问题(LeetCode 63)
3.1 问题变化与挑战
现在网格中可能存在障碍物(1表示障碍,0表示空位),这会如何影响我们的解法?
关键变化:
- 初始位置或目标位置有障碍时直接返回0
- 遇到障碍物时该位置的路径数为0
- 初始化边界时,一旦遇到障碍物,后续位置都不可达
3.2 动态规划解法调整
3.2.1 状态定义
依然使用dp[i][j],但要考虑障碍物:
- 如果obstacleGrid[i][j] == 1,则dp[i][j] = 0
- 否则dp[i][j] = dp[i-1][j] + dp[i][j-1]
3.2.2 边界初始化技巧
第一行和第一列的初始化需要特殊处理:
# 初始化第一列 for i in range(m): if obstacleGrid[i][0] == 1: for k in range(i, m): dp[k][0] = 0 break dp[i][0] = 1 # 初始化第一行 for j in range(n): if obstacleGrid[0][j] == 1: for k in range(j, n): dp[0][k] = 0 break dp[0][j] = 13.2.3 完整代码实现
def uniquePathsWithObstacles(obstacleGrid: List[List[int]]) -> int: m, n = len(obstacleGrid), len(obstacleGrid[0]) if obstacleGrid[0][0] == 1 or obstacleGrid[-1][-1] == 1: return 0 dp = [[0]*n for _ in range(m)] dp[0][0] = 1 # 初始化第一列 for i in range(1, m): dp[i][0] = 0 if obstacleGrid[i][0] == 1 else dp[i-1][0] # 初始化第一行 for j in range(1, n): dp[0][j] = 0 if obstacleGrid[0][j] == 1 else dp[0][j-1] for i in range(1, m): for j in range(1, n): if obstacleGrid[i][j] == 1: dp[i][j] = 0 else: dp[i][j] = dp[i-1][j] + dp[i][j-1] return dp[-1][-1]3.3 空间优化方案
同样可以优化为一维数组:
def uniquePathsWithObstacles(obstacleGrid: List[List[int]]) -> int: n = len(obstacleGrid[0]) dp = [0] * n dp[0] = 1 if obstacleGrid[0][0] == 0 else 0 for row in obstacleGrid: for j in range(n): if row[j] == 1: dp[j] = 0 elif j > 0: dp[j] += dp[j-1] return dp[-1]4. 常见错误与调试技巧
4.1 边界条件处理不当
- 忘记检查起点或终点是否有障碍物
- 边界初始化时遇到障碍物后没有终止循环
- 二维数组初始化错误(浅拷贝问题)
4.2 状态转移方程错误
- 在障碍物位置没有置0
- 忽略了障碍物对相邻位置的影响
4.3 调试建议
- 先用小规模测试用例验证(如2×2带障碍)
- 打印中间dp表检查数值是否符合预期
- 特别注意第一行和第一列的初始化
5. 复杂度分析与优化思路
5.1 时间复杂度
两种解法都是O(m×n),因为需要遍历整个网格。
5.2 空间复杂度
- 基础解法:O(m×n)
- 优化解法:O(n)或O(m)(取决于实现方式)
5.3 进一步优化方向
- 记忆化搜索:适用于稀疏障碍物场景
- 并行计算:对于超大网格可考虑并行处理
- 数学优化:在无障碍情况下使用组合公式
6. 实际应用场景延伸
路径问题在实际中有广泛的应用:
- 机器人路径规划
- 游戏中的AI移动计算
- 交通流量预测
- VLSI芯片布线
理解这类问题的解法后,可以轻松扩展到:
- 带权重的路径问题(求最大/最小路径和)
- 三维空间的移动问题
- 存在移动限制的场景(如某些位置只能特定方向移动)
7. 个人实战经验分享
在多次面试中考察这类题目时,我发现候选人常犯的几个错误:
- 没有先处理特殊case(如起点就是障碍)
- 边界初始化逻辑不完整
- 空间优化时覆盖了还需要使用的值
一个实用的调试技巧:在纸上画出3×3的网格,手动填写dp表,这能快速发现逻辑漏洞。对于障碍物版本,建议先设计一个简单的障碍布局,如中心位置有障碍,然后手动计算验证代码输出。