1. 问题描述与理解
这道题目要求我们计算在m×n的网格中,从左上角到右下角的所有可能路径数量。每次移动只能向右或向下,且不能回退。这类网格路径问题在算法面试中非常经典,也是动态规划(Dynamic Programming)的入门级练习题。
我第一次遇到这个问题时,直觉告诉我可以用递归来解决——毕竟每次只有两种选择,看起来像是二叉树遍历的变种。但实际动手后才发现,当网格尺寸稍大时(比如20×20),递归解法就会因为重复计算导致性能急剧下降。这让我意识到必须寻找更高效的解法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 递归解法与优化思路
2.1 基础递归实现
最直观的解法确实是递归:从起点(0,0)出发,每次选择向右或向下移动,直到到达终点(m-1,n-1)。递归的终止条件是越界或到达终点:
python复制def uniquePaths(m, n):
def dfs(i, j):
if i >= m or j >= n: # 越界
return 0
if i == m-1 and j == n-1: # 到达终点
return 1
return dfs(i+1, j) + dfs(i, j+1)
return dfs(0, 0)
这个解法虽然正确,但时间复杂度高达O(2^(m+n)),因为每个位置都会分裂出两个递归调用。对于m=n=20的情况,递归调用次数会超过百万级别。
2.2 记忆化递归优化
观察到递归过程中存在大量重复计算(比如从(1,1)出发的路径会被多次计算),我们可以引入记忆化存储:
python复制def uniquePaths(m, n):
memo = {}
def dfs(i, j):
if (i,j) in memo:
return memo[(i,j)]
if i >= m or j >= n:
return 0
if i == m-1 and j == n-1:
return 1
memo[(i,j)] = dfs(i+1, j) + dfs(i, j+1)
return memo[(i,j)]
return dfs(0, 0)
这种优化将时间复杂度降低到O(m×n),因为每个网格点只需计算一次。空间复杂度也是O(m×n)用于存储记忆化结果。
提示:记忆化递归是理解动态规划的重要过渡步骤。它保留了递归的直观性,又通过存储中间结果避免了重复计算。
3. 动态规划解法详解
3.1 状态定义与转移方程
动态规划的核心在于定义状态和建立状态转移关系。对于本题:
- 状态定义:dp[i][j]表示从(0,0)到(i,j)的路径数量
- 边界条件:
- 第一行dp[0][j] = 1(只能一直向右)
- 第一列dp[i][0] = 1(只能一直向下)
- 状态转移方程:
- dp[i][j] = dp[i-1][j] + dp[i][j-1] (来自上方或左方的路径之和)
3.2 完整DP实现
基于上述分析,我们可以得到标准的二维DP解法:
python复制def uniquePaths(m, n):
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]
这个解法的时间复杂度和空间复杂度都是O(m×n)。在LeetCode上测试,对于m=n=100的情况也能在毫秒级完成计算。
3.3 空间优化技巧
观察状态转移方程可以发现,dp[i][j]只依赖于当前行和前一行,因此可以将空间复杂度优化到O(n):
python复制def uniquePaths(m, n):
dp = [1]*n
for _ in range(1, m):
for j in range(1, n):
dp[j] += dp[j-1]
return dp[-1]
这种优化在面试中常常被追问,因为它展示了候选人对空间复杂度的敏感度。实际应用中,当网格非常大时(比如1000×1000),这种优化能显著减少内存使用。
4. 数学组合数解法
4.1 组合数学原理
这个问题还可以从组合数学的角度理解:从起点到终点总共需要移动(m-1)+(n-1)步,其中包含(m-1)次向下和(n-1)次向右移动。因此路径总数等于从(m+n-2)步中选择(m-1)步向下(或(n-1)步向右)的组合数:
C(m+n-2, m-1) = (m+n-2)! / [(m-1)! × (n-1)!]
4.2 实现与注意事项
python复制import math
def uniquePaths(m, n):
return math.comb(m+n-2, m-1)
虽然代码简洁,但在实现时需要注意:
- 阶乘计算可能溢出:当m+n较大时(如>30),直接计算阶乘会导致整数溢出
- 计算效率:可以优化组合数计算过程,避免完整计算大数阶乘
注意:在Python中math.comb()已经处理了大数问题,但在其他语言如C++中需要特别注意溢出问题。
5. 不同解法的对比分析
| 解法类型 | 时间复杂度 | 空间复杂度 | 适用场景 | 实现难度 |
|---|---|---|---|---|
| 基础递归 | O(2^(m+n)) | O(m+n) | 仅用于理解问题 | 简单 |
| 记忆化递归 | O(m×n) | O(m×n) | 中等规模网格 | 中等 |
| 标准DP | O(m×n) | O(m×n) | 各种规模 | 中等 |
| 优化DP | O(m×n) | O(n) | 大规模网格 | 较难 |
| 组合数学 | O(min(m,n)) | O(1) | 数学解法 | 简单 |
在实际面试中,建议按照以下顺序展示解法:
- 先提出递归思路(展示问题理解)
- 指出递归的问题(展示分析能力)
- 提出记忆化递归优化(过渡到DP)
- 给出标准DP解法(主要解法)
- 讨论空间优化(加分项)
- 最后提组合数解法(展示数学素养)
6. 常见错误与调试技巧
6.1 边界条件处理
新手常犯的错误包括:
- 忘记处理m或n为1的特殊情况(只有一条路径)
- 错误初始化DP数组(第一行和第一列应全为1)
- 数组索引越界(特别是在优化空间版本中)
调试建议:
- 先用小规模测试用例(如2×2)手动验证
- 打印DP表格检查中间结果
6.2 整数溢出问题
在组合数解法中,当m+n较大时:
- C++/Java等语言需要使用long类型
- 可以改用递推方式计算组合数,避免直接计算大阶乘
6.3 性能优化技巧
对于特别大的网格(如1000×1000):
- DP解法仍是最可靠的
- 可以预先计算阶乘表(如果多次调用)
- 考虑并行计算(将网格分块处理)
7. 问题变种与扩展
7.1 带障碍物的版本
LeetCode第63题"不同路径II"在网格中加入了障碍物。解法需要:
- 遇到障碍物时dp[i][j]=0
- 更谨慎地初始化第一行/列(一旦遇到障碍物,后续都为0)
7.2 三维网格路径
如果扩展为三维网格(可以向右/向下/向内移动),解法思路类似:
- DP状态变为dp[i][j][k]
- 状态转移来自三个方向的和
7.3 加权最短路径
如果每个网格有移动成本,求最小成本路径:
- 改用min()而不是sum()
- 初始化时需要累加第一行/列的成本
8. 实际应用场景
这类网格路径问题在实际中有诸多应用:
- 机器人导航中的路径规划
- 电路板布线问题
- 游戏中的AI移动决策
- 生物信息学中的序列比对
理解这个基础问题有助于解决更复杂的实际工程问题。我在工作中就曾用类似的DP思想优化过物流路径规划系统,将计算时间从小时级降低到秒级。
