想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。
📖 第80课:不同路径
模块:动态规划 | 难度:Medium ⭐⭐ LeetCode 链接:leetcode.cn/problems/un… 前置知识:第71课(爬楼梯)、第72课(杨辉三角) 预计学习时间:20分钟
🎯 题目描述
机器人位于一个 m × n 网格的左上角,每次只能向下或向右移动一步,试问到达右下角总共有多少条不同的路径。
示例:
输入:m = 3, n = 7
输出:28
解释:从左上角到右下角共有28条不同路径
约束条件:
1 <= m, n <= 100- 机器人只能向下或向右移动
🧪 边界用例(面试必考)
| 用例类型 | 输入 | 期望输出 | 考察点 |
|---|---|---|---|
| 最小网格 | m=1, n=1 | 1 | 起点即终点 |
| 单行 | m=1, n=7 | 1 | 只能一直向右 |
| 单列 | m=3, n=1 | 1 | 只能一直向下 |
| 正方形 | m=3, n=3 | 6 | 基本功能 |
| 大规模 | m=100, n=100 | — | 性能边界 |
💡 思路引导
生活化比喻
想象你在一个街区的西北角,要走到东南角的家,街区是棋盘状的,你每次只能向东或向南走一个街区。
🐌 笨办法:枚举所有可能的路线,数一数有多少条。假如是10×10的街区,可能的路线数是组合数 C(18,9) ≈ 48620,全部枚举会很慢。
🚀 聪明办法:站在某个路口时,到达这个路口的方法数 = "从上面来"的方法数 + "从左边来"的方法数。从起点开始,逐步推算出每个路口的到达方法数,最后得到终点的答案。
关键洞察
到达 (i, j) 的路径数 = 到达 (i-1, j) 的路径数 + 到达 (i, j-1) 的路径数
🧠 解题思维链
这一节模拟你在面试中"从零开始思考"的过程。
Step 1:理解题目 → 锁定输入输出
- 输入:网格大小 m(行数)、n(列数),范围 1~100
- 输出:整数,表示不同路径的总数
- 限制:只能向下或向右移动
Step 2:先想笨办法(暴力法)
用递归枚举所有路径:从 (0,0) 出发,每次选择"向下"或"向右",直到到达 (m-1, n-1)。
- 时间复杂度:O(2^(m+n)) — 每个位置两种选择,总共需要 m+n-2 步
- 瓶颈在哪:指数级的枚举,存在大量重复计算
Step 3:瓶颈分析 → 优化方向
递归中存在重复子问题。例如到达 (2,3) 的路径,可能从 (1,3) 或 (2,2) 到达,而这两个位置的路径数都会被重复计算多次。
- 核心问题:每次都要重新计算"到达某个位置的路径数"
- 优化思路:能不能记住"到达每个位置的路径数",避免重复计算?
Step 4:选择武器
- 选用:网格动态规划
- 理由:这是典型的"在网格中计数路径"问题,状态转移清晰(当前格子依赖上方和左方)
🔑 模式识别提示:当题目出现"网格中从起点到终点的路径计数/最值",优先考虑"网格DP"
🔑 解法一:递归+记忆化(朴素法)
思路
用递归计算到达每个位置的路径数,用哈希表缓存已计算的结果,避免重复计算。
图解过程
网格 3×3:
(0,0) → → → (0,2)
↓ ↓
(1,0) → → → (1,2)
↓ ↓
(2,0) → → → (2,2)
递归树(省略重复节点):
paths(2,2)
/ \
paths(1,2) paths(2,1)
/ \ / \
paths(0,2) ... paths(1,1) ...
Python代码
def uniquePaths_memo(m: int, n: int) -> int:
"""
解法一:递归+记忆化
思路:递归计算每个位置的路径数,用哈希表缓存
"""
memo = {}
def dfs(i: int, j: int) -> int:
# 边界:到达第一行或第一列,只有1条路径
if i == 0 or j == 0:
return 1
# 查缓存
if (i, j) in memo:
return memo[(i, j)]
# 状态转移:当前位置 = 上方 + 左方
result = dfs(i - 1, j) + dfs(i, j - 1)
memo[(i, j)] = result
return result
return dfs(m - 1, n - 1)
# ✅ 测试
print(uniquePaths_memo(3, 7)) # 期望输出:28
print(uniquePaths_memo(3, 2)) # 期望输出:3
print(uniquePaths_memo(1, 1)) # 期望输出:1
复杂度分析
- 时间复杂度:O(m × n) — 每个格子计算一次,共 m×n 个格子
- 具体地说:如果 m=3, n=7,大约需要 21 次计算
- 空间复杂度:O(m × n) — 递归栈 + 哈希表
优缺点
- ✅ 递归思路直观,易于理解状态转移
- ❌ 空间开销较大(递归栈+哈希表),可以优化
🏆 解法二:二维DP(最优解)
优化思路
用二维数组 dp[i][j] 记录到达每个位置的路径数,从左上角开始逐行/逐列填表,最后返回右下角的值。
💡 关键想法:定义 dp[i][j] 为"到达位置 (i, j) 的路径数"
图解过程
网格 3×7,初始化第一行和第一列为1:
0 1 2 3 4 5 6
0 1 1 1 1 1 1 1 ← 第一行只能一直向右
1 1 ? ? ? ? ? ?
2 1 ? ? ? ? ? ?
↑ 第一列只能一直向下
填表过程:
dp[1][1] = dp[0][1] + dp[1][0] = 1 + 1 = 2
dp[1][2] = dp[0][2] + dp[1][1] = 1 + 2 = 3
dp[1][3] = dp[0][3] + dp[1][2] = 1 + 3 = 4
...
最终结果:
0 1 2 3 4 5 6
0 1 1 1 1 1 1 1
1 1 2 3 4 5 6 7
2 1 3 6 10 15 21 28 ← dp[2][6] = 28
Python代码
def uniquePaths(m: int, n: int) -> int:
"""
解法二:二维DP(最优解)
思路:dp[i][j] = 到达位置 (i,j) 的路径数
"""
# 创建 m×n 的DP表
dp = [[0] * n for _ in range(m)]
# 初始化第一行和第一列
for i in range(m):
dp[i][0] = 1 # 第一列只有1条路径(一直向下)
for j in range(n):
dp[0][j] = 1 # 第一行只有1条路径(一直向右)
# 填表:dp[i][j] = dp[i-1][j] + dp[i][j-1]
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[m - 1][n - 1]
# ✅ 测试
print(uniquePaths(3, 7)) # 期望输出:28
print(uniquePaths(3, 2)) # 期望输出:3
print(uniquePaths(1, 1)) # 期望输出:1
复杂度分析
- 时间复杂度:O(m × n) — 填满整个DP表
- 空间复杂度:O(m × n) — DP表的大小
⚡ 解法三:一维DP空间优化(进阶)
优化思路
观察状态转移方程 dp[i][j] = dp[i-1][j] + dp[i][j-1],当前行只依赖上一行和当前行左边的值,可以用滚动数组优化空间到 O(n)。
图解过程
用一维数组 dp[],每次更新代表当前行:
初始化:dp = [1, 1, 1, 1, 1, 1, 1] (第一行)
更新第2行:
dp[1] = dp[1] + dp[0] = 1 + 1 = 2
dp[2] = dp[2] + dp[1] = 1 + 2 = 3
dp[3] = dp[3] + dp[2] = 1 + 3 = 4
...
结果:dp = [1, 2, 3, 4, 5, 6, 7]
更新第3行:
dp[1] = dp[1] + dp[0] = 2 + 1 = 3
dp[2] = dp[2] + dp[1] = 3 + 3 = 6
...
结果:dp = [1, 3, 6, 10, 15, 21, 28]
Python代码
def uniquePaths_optimized(m: int, n: int) -> int:
"""
解法三:一维DP空间优化
思路:用滚动数组,每次更新代表当前行
"""
# 初始化为第一行,全部为1
dp = [1] * n
# 逐行更新
for i in range(1, m):
for j in range(1, n):
dp[j] = dp[j] + dp[j - 1]
# dp[j] 原值是上一行的值(对应 dp[i-1][j])
# dp[j-1] 是当前行左边的值(对应 dp[i][j-1])
return dp[n - 1]
# ✅ 测试
print(uniquePaths_optimized(3, 7)) # 期望输出:28
print(uniquePaths_optimized(3, 2)) # 期望输出:3
复杂度分析
- 时间复杂度:O(m × n) — 与二维DP相同
- 空间复杂度:O(n) — 只需一维数组
🐍 Pythonic 写法
利用组合数学公式,这道题本质是求组合数 C(m+n-2, m-1):
from math import comb
def uniquePaths_math(m: int, n: int) -> int:
"""数学法:组合数 C(m+n-2, m-1)"""
return comb(m + n - 2, m - 1)
解释:从 (0,0) 到 (m-1,n-1) 需要向下 m-1 步,向右 n-1 步,总共 m+n-2 步。问题等价于"从 m+n-2 步中选 m-1 步向下",即组合数 C(m+n-2, m-1)。
⚠️ 面试建议:先写DP展示思路,再提数学解法展示知识广度。 面试官更看重你的DP思维,而非公式记忆。
📊 解法对比
| 维度 | 解法一:递归记忆化 | 🏆 解法二:二维DP(最优) | 解法三:一维DP优化 |
|---|---|---|---|
| 时间复杂度 | O(m × n) | O(m × n) | O(m × n) |
| 空间复杂度 | O(m × n) | O(m × n) | O(n) ← 最优 |
| 代码难度 | 中等 | 简单 ← 最清晰 | 中等 |
| 面试推荐 | ⭐⭐ | ⭐⭐⭐ ← 首选 | ⭐⭐ |
| 适用场景 | 理解状态转移 | 面试首选,代码清晰 | 进阶优化,展示优化能力 |
为什么解法二是最优解:
- 代码最清晰易懂,DP表直观体现状态转移
- 时间复杂度 O(m×n) 已经是理论最优(至少要填满整个网格)
- 虽然空间可以优化到 O(n),但二维版本更容易理解和扩展
面试建议:
- 先用30秒口述递归思路,表明你理解状态转移
- 立即写出🏆最优解(二维DP),展示DP建表能力
- 重点讲解核心思想:"每个格子的路径数 = 上方格子 + 左方格子"
- 强调边界初始化:第一行和第一列都是1
- 如果时间充裕,提一维DP优化展示进阶能力
🎤 面试现场
模拟面试中的完整对话流程,帮你练习"边想边说"。
面试官:请你解决一下这道题。
你:(审题30秒)好的,这道题要求计算从左上角到右下角的不同路径数,每次只能向下或向右移动。
我的第一个想法是用递归:paths(i, j) = paths(i-1, j) + paths(i, j-1),边界是第一行和第一列都是1。但递归会有重复计算,时间复杂度是指数级。
优化方法是用动态规划,建一个 m×n 的DP表,dp[i][j] 表示到达位置 (i,j) 的路径数。状态转移方程是 dp[i][j] = dp[i-1][j] + dp[i][j-1],时间复杂度是 O(m×n)。
面试官:很好,请写一下代码。
你:(边写边说)
def uniquePaths(m, n):
# 创建DP表
dp = [[0] * n for _ in range(m)]
# 初始化第一行和第一列为1
for i in range(m):
dp[i][0] = 1
for j in range(n):
dp[0][j] = 1
# 填表:当前格子 = 上方 + 左方
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[m - 1][n - 1]
面试官:能优化空间吗?
你:可以!因为每行只依赖上一行,可以用一维数组滚动更新。初始化 dp=[1]*n,然后逐行更新,dp[j] = dp[j] + dp[j-1],空间复杂度降到 O(n)。
面试官:测试一下?
你:用示例 m=3, n=3 走一遍:
- 初始化第一行:[1,1,1]
- 更新第二行:[1,2,3]
- 更新第三行:[1,3,6] ✅ 结果是6,符合预期。
高频追问
| 追问 | 应答策略 |
|---|---|
| "还有更优解吗?" | 时间 O(m×n) 已经是最优,空间可以优化到 O(min(m,n)),选择较短的维度作为滚动数组 |
| "如果有障碍物呢?" | 遇到障碍物时 dp[i][j]=0,其他逻辑不变(参考 LeetCode 63) |
| "能用数学公式吗?" | 可以用组合数 C(m+n-2, m-1),但面试更看重DP思维 |
| "实际工程中怎么用?" | 游戏中计算路径数、网络路由计数、电路设计中的布线方案计数 |
🎓 知识点总结
Python技巧卡片 🐍
# 技巧1:二维列表初始化 — 注意不能用 [[0]*n]*m
dp = [[0] * n for _ in range(m)] # 正确:每行独立
# dp = [[0] * n] * m # 错误:所有行指向同一对象
# 技巧2:一维滚动数组 — 原地更新节省空间
dp[j] = dp[j] + dp[j - 1] # dp[j]原值是上一行,dp[j-1]是当前行左边
💡 底层原理(选读)
为什么一维DP要从左到右更新?
在滚动数组优化中,
dp[j] = dp[j] + dp[j-1]:
- dp[j] 原值代表上一行的值(对应二维的 dp[i-1][j])
- dp[j-1] 已经被更新,代表当前行左边的值(对应二维的 dp[i][j-1])
从左到右更新确保:
- 更新 dp[j] 时,dp[j-1] 已经是当前行的值
- dp[j] 的原值还保留着上一行的值
杨辉三角与本题的联系: 如果画出DP表,会发现数字排列类似杨辉三角(帕斯卡三角形)。 本质原因:组合数递推公式 C(n,k) = C(n-1,k) + C(n-1,k-1) 与本题状态转移方程一致。
算法模式卡片 📐
- 模式名称:网格DP(路径计数)
- 适用条件:在二维网格中从起点到终点的路径计数/最值问题
- 识别关键词:"网格中移动"、"只能向下/向右"、"路径数/最小路径和"
- 模板代码:
def grid_dp(m, n):
dp = [[0] * n for _ in range(m)]
# 初始化第一行和第一列
for i in range(m):
dp[i][0] = 1 # 或其他初值
for j in range(n):
dp[0][j] = 1
# 填表
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[m-1][n-1]
易错点 ⚠️
-
二维列表初始化错误:用
[[0]*n]*m导致所有行指向同一对象- 错误:
dp = [[0] * n] * m - 正确:
dp = [[0] * n for _ in range(m)]
- 错误:
-
边界初始化遗漏:忘记初始化第一行和第一列
- 错误:直接从 (1,1) 开始填表,导致第一行第一列全是0
- 正确:先初始化
dp[i][0]=1和dp[0][j]=1
-
返回值下标错误:返回 dp[m][n] 而非 dp[m-1][n-1]
- 错误:
return dp[m][n](数组越界) - 正确:
return dp[m-1][n-1](右下角坐标)
- 错误:
🏗️ 工程实战(选读)
这个算法思想在真实项目中的应用,让你知道"学了有什么用"。
- 场景1:游戏关卡设计 — 计算迷宫中从起点到终点的可能路线数
- 场景2:物流路径规划 — 在城市网格中计算配送路线的多样性
- 场景3:芯片设计 — 计算电路板上从输入端到输出端的布线方案数
🏋️ 举一反三
完成本课后,试试这些同类题目来巩固知识:
| 题目 | 难度 | 相关知识点 | 提示 |
|---|---|---|---|
| LeetCode 63. 不同路径II | Medium | 网格DP+障碍物 | 遇到障碍物时 dp[i][j]=0 |
| LeetCode 64. 最小路径和 | Medium | 网格DP求最值 | 状态转移改为取 min |
| LeetCode 120. 三角形最小路径和 | Medium | 变形网格DP | 每行长度不同,注意边界 |
📝 课后小测
试试这道变体题,不要看答案,自己先想5分钟!
题目:在 m×n 网格中,如果某些格子有障碍物(标记为1),机器人无法通过,其他格子标记为0。计算从左上角到右下角的不同路径数。例如 grid=[[0,0,0],[0,1,0],[0,0,0]],返回2。
💡 提示(实在想不出来再点开)
遇到障碍物时,该位置的路径数为0。状态转移方程变为:如果 grid[i][j]==1,则 dp[i][j]=0;否则 dp[i][j] = dp[i-1][j] + dp[i][j-1]。
✅ 参考答案
def uniquePathsWithObstacles(obstacleGrid):
m, n = len(obstacleGrid), len(obstacleGrid[0])
if obstacleGrid[0][0] == 1 or obstacleGrid[m-1][n-1] == 1:
return 0 # 起点或终点有障碍
dp = [[0] * n for _ in range(m)]
dp[0][0] = 1
# 初始化第一行
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):
dp[i][0] = 0 if obstacleGrid[i][0] == 1 else dp[i-1][0]
# 填表
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[m-1][n-1]
核心思路:在原来的DP基础上增加障碍物判断,遇到障碍物时路径数为0,其他逻辑不变。注意初始化时也要检查障碍物。
如果这篇内容对你有帮助,推荐收藏 AI Compass:github.com/tingaicompa… 更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。