1. 问题背景与理解
Unique Paths 是 LeetCode 上经典的动态规划入门题目,编号62。题目描述很简单:一个机器人位于 m x n 网格的左上角,每次只能向下或向右移动一步,问到达网格右下角有多少条不同的路径。
这个问题看似简单,却包含了动态规划最核心的思想。我第一次遇到这个问题时,直觉告诉我可以用递归解决,但很快发现递归解法在较大网格时会超时。这促使我深入思考更高效的解法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础解法分析
2.1 递归解法(暴力搜索)
最直观的解法是使用递归。对于网格中的每个点(i,j),路径数等于从(i+1,j)和(i,j+1)出发的路径数之和。递归终止条件是到达网格边界。
python复制def uniquePaths(m: int, n: int) -> int:
def dfs(i, j):
if i == m-1 or j == n-1:
return 1
return dfs(i+1, j) + dfs(i, j+1)
return dfs(0, 0)
这个解法虽然正确,但时间复杂度高达O(2^(m+n)),因为存在大量重复计算。在LeetCode上提交会超时。
2.2 记忆化递归(自顶向下DP)
为了优化递归解法,我们可以加入记忆化存储。创建一个memo数组保存已经计算过的结果,避免重复计算。
python复制def uniquePaths(m: int, n: int) -> int:
memo = [[-1]*n for _ in range(m)]
def dfs(i, j):
if i == m-1 or j == n-1:
return 1
if memo[i][j] != -1:
return memo[i][j]
memo[i][j] = dfs(i+1, j) + dfs(i, j+1)
return memo[i][j]
return dfs(0, 0)
这个优化将时间复杂度降到了O(mn),因为每个网格点只计算一次。空间复杂度也是O(mn)。
3. 标准动态规划解法
3.1 自底向上DP
更标准的动态规划解法是从网格右下角开始,逐步填充dp表。dp[i][j]表示从(i,j)到终点的路径数。
python复制def uniquePaths(m: int, n: int) -> int:
dp = [[1]*n for _ in range(m)]
for i in range(m-2, -1, -1):
for j in range(n-2, -1, -1):
dp[i][j] = dp[i+1][j] + dp[i][j+1]
return dp[0][0]
这个解法同样具有O(m*n)的时间和空间复杂度,但通常比记忆化递归运行更快,因为避免了递归调用的开销。
3.2 空间优化DP
观察状态转移方程可以发现,dp[i][j]只依赖于右边和下边的值。因此可以将空间复杂度优化到O(n)。
python复制def uniquePaths(m: int, n: int) -> int:
dp = [1]*n
for _ in range(m-1):
for j in range(n-2, -1, -1):
dp[j] += dp[j+1]
return dp[0]
这个优化版本在实际面试中非常受欢迎,因为它展示了候选人对空间复杂度的敏感度。
4. 数学组合解法
4.1 组合数学思路
这个问题实际上可以用组合数学来解决。从起点到终点需要移动(m-1)+(n-1)步,其中(m-1)步向下,(n-1)步向右。不同的路径数就是这些步的排列组合数。
路径总数等于从(m+n-2)步中选择(m-1)步向下的组合数,即C(m+n-2, m-1)。
4.2 实现与注意事项
python复制import math
def uniquePaths(m: int, n: int) -> int:
return math.comb(m+n-2, m-1)
虽然这个解法看起来简洁,但在实际实现时需要注意:
- 大数计算可能溢出(Python的整数不会溢出,但其他语言需要考虑)
- 组合数计算的时间复杂度是O(min(m,n)),比DP解法更快
- 浮点数精度问题(如果使用阶乘相除的方式计算)
5. 变种与扩展问题
5.1 网格中存在障碍物
LeetCode 63题"Unique Paths II"在网格中加入了障碍物。这时DP解法需要稍作修改:
python复制def uniquePathsWithObstacles(obstacleGrid: List[List[int]]) -> int:
m, n = len(obstacleGrid), len(obstacleGrid[0])
dp = [[0]*n for _ in range(m)]
# 初始化第一行和第一列
dp[0][0] = 1 if obstacleGrid[0][0] == 0 else 0
for i in range(1, m):
dp[i][0] = dp[i-1][0] if obstacleGrid[i][0] == 0 else 0
for j in range(1, n):
dp[0][j] = dp[0][j-1] if obstacleGrid[0][j] == 0 else 0
for i in range(1, m):
for j in range(1, n):
if obstacleGrid[i][j] == 0:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[-1][-1]
5.2 三维空间中的路径问题
如果问题扩展到三维空间(x×y×z的立方体),解法思路类似,只是状态转移方程变为:
dp[i][j][k] = dp[i-1][j][k] + dp[i][j-1][k] + dp[i][j][k-1]
6. 常见错误与调试技巧
6.1 边界条件处理
新手常犯的错误是边界条件处理不当。例如:
- 忘记处理1×1网格的特殊情况
- 在存在障碍物时,初始化第一行和第一列的逻辑错误
- 数组越界访问(特别是在使用空间优化DP时)
6.2 测试用例建议
验证代码时建议测试以下情况:
- 最小网格:1×1
- 单行网格:1×n
- 单列网格:m×1
- 常规网格:3×7
- 大网格:100×100(测试性能)
- 存在障碍物的各种情况
7. 性能对比与选择建议
不同解法在不同场景下的表现:
- 小网格(m,n < 20):所有解法都可以
- 中等网格(20 ≤ m,n ≤ 100):DP解法更优
- 大网格(m,n > 100):组合数学解法最快
- 存在障碍物:必须使用DP解法
在面试中,建议按以下顺序展示:
- 先提出递归思路(展示问题理解)
- 指出重复计算问题(展示分析能力)
- 提出记忆化递归(展示优化意识)
- 转换为自底向上DP(展示DP掌握程度)
- 空间优化(展示深入理解)
- 最后提组合数学解法(展示数学素养)
8. 实际应用场景
Unique Paths问题看似简单,但其解法思想可以应用于:
- 棋盘类游戏路径计算
- 机器人导航路径规划
- 网络路由选择
- 投资组合优化
- 生物信息学中的序列比对
理解这个问题的各种解法,不仅是为了通过面试,更是为了培养解决实际问题的思维方式。动态规划是算法设计中最强大的工具之一,掌握它能够帮助你解决许多看似复杂的问题。
