📌 题目链接:62. 不同路径 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:动态规划、组合数学、数学
⏱️ 目标时间复杂度:O(mn)(DP) / O(min(m, n))(组合数)
💾 空间复杂度:O(mn) → O(min(m, n))(滚动数组优化) / O(1)(组合数)
🧠 题目分析
题目描述了一个经典的网格路径问题:一个机器人从 m x n 网格的左上角出发,每次只能 向右 或 向下 移动一步,问有多少种不同的路径可以到达右下角。
这个问题看似简单,但背后蕴含了两种非常重要的算法思想:
- 动态规划(Dynamic Programming):适用于具有最优子结构和重叠子问题的问题;
- 组合数学(Combinatorics):将路径问题转化为排列组合问题,实现更优时空复杂度。
在面试中,这类问题常被用来考察候选人对 状态转移建模能力 和 数学思维转换能力 的掌握程度。
🧩 核心算法及代码讲解
✅ 方法一:动态规划(DP)
💡 核心思想
设 dp[i][j] 表示从起点 (0, 0) 到达位置 (i, j) 的不同路径数。由于机器人只能向右或向下移动,因此:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i ] [ j − 1 ] dp[i][j] = dp[i-1][j] + dp[i][j-1] </math>dp[i][j]=dp[i−1][j]+dp[i][j−1]
边界条件:
- 第一行所有格子只能由左边到达 →
dp[0][j] = 1 - 第一列所有格子只能由上面到达 →
dp[i][0] = 1
📝 代码详解(含行注释)
cpp
class Solution {
public:
int uniquePaths(int m, int n) {
// 创建二维 DP 表,初始化为 0
vector<vector<int>> f(m, vector<int>(n));
// 初始化第一列为 1:只能从上方来
for (int i = 0; i < m; ++i) {
f[i][0] = 1;
}
// 初始化第一行为 1:只能从左方来
for (int j = 0; j < n; ++j) {
f[0][j] = 1;
}
// 填充其余格子:当前路径数 = 上方 + 左方
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
f[i][j] = f[i - 1][j] + f[i][j - 1];
}
}
// 返回右下角的路径总数
return f[m - 1][n - 1];
}
};
🔁 空间优化:滚动数组(面试加分项!)
注意到 dp[i][j] 只依赖于上一行和当前行的左侧值,因此可以用一维数组滚动更新:
cpp
vector<int> f(n, 1); // 初始化为 1,对应第一行
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
f[j] += f[j - 1]; // f[j] 原为上一行的值,f[j-1] 是当前行已更新的左值
}
}
return f[n - 1];
✅ 面试提示 :当被问到"能否优化空间?"时,务必提到滚动数组技巧,并说明其原理------状态只依赖前一行/前一列。
✅ 方法二:组合数学(数学法)
💡 核心思想
从 (0,0) 到 (m-1,n-1),总共需要走:
- 向下:
m - 1步 - 向右:
n - 1步
共m + n - 2步。
问题转化为:在这 m+n-2 步中,选择 m-1 步用于向下(其余自动为向右),即求组合数:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> C m + n − 2 m − 1 = ( m + n − 2 ) ! ( m − 1 ) ! ⋅ ( n − 1 ) ! C_{m+n-2}^{m-1} = \frac{(m+n-2)!}{(m-1)! \cdot (n-1)!} </math>Cm+n−2m−1=(m−1)!⋅(n−1)!(m+n−2)!
但直接计算阶乘会溢出,因此采用递推式避免大数:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> C = ∏ k = 1 m − 1 n − 1 + k k C = \prod_{k=1}^{m-1} \frac{n - 1 + k}{k} </math>C=k=1∏m−1kn−1+k
等价于循环累乘并整除,保证中间结果始终为整数(因为组合数必为整数)。
📝 代码详解(含行注释)
cpp
class Solution {
public:
int uniquePaths(int m, int n) {
long long ans = 1;
// 为了减少循环次数,确保 m <= n(可选优化)
if (m > n) swap(m, n);
// 计算 C(m+n-2, m-1) = ∏_{y=1}^{m-1} (n - 1 + y) / y
for (int y = 1; y < m; ++y) {
ans = ans * (n - 1 + y) / y; // 先乘后除,避免浮点误差
}
return (int)ans;
}
};
✅ 关键细节 :必须 先乘后除,且按顺序进行,才能保证每一步都是整数(数学性质:组合数是整数,且该递推过程保持整除性)。
❗ 常见错误 :若写成ans *= (n-1+y)/y,由于整数除法截断,会导致错误!
🧭 解题思路(分步拆解)
- 理解移动规则:只能右或下 → 路径无回头,问题具有无后效性。
- 识别子问题 :到达
(i,j)的路径数 = 到达(i-1,j)+(i,j-1)。 - 确定边界:第一行/列只有 1 条路径。
- 选择方法 :
- 若需展示 DP 思维 → 用二维 DP;
- 若要最优性能 → 用组合数学;
- 若面试官追问空间优化 → 提出滚动数组。
- 验证小例子 :如
m=3, n=2→ 手动枚举 3 条路径,验证逻辑正确。
📊 算法分析
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 二维 DP | O(mn) | O(mn) | 教学、清晰表达状态转移 |
| 滚动数组 DP | O(mn) | O(min(m,n)) | 面试中展示空间优化能力 |
| 组合数学 | O(min(m,n)) | O(1) | 追求极致效率,数学功底强 |
💡 面试建议:
- 先写 DP 解法(稳妥、易理解);
- 再提出组合数学优化(展现深度);
- 最后讨论空间优化(体现工程思维)。
💻 代码
C++ 完整代码
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> f(m, vector<int>(n));
for (int i = 0; i < m; ++i) {
f[i][0] = 1;
}
for (int j = 0; j < n; ++j) {
f[0][j] = 1;
}
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
f[i][j] = f[i - 1][j] + f[i][j - 1];
}
}
return f[m - 1][n - 1];
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
cout << sol.uniquePaths(3, 7) << "\n"; // 输出: 28
cout << sol.uniquePaths(3, 2) << "\n"; // 输出: 3
cout << sol.uniquePaths(7, 3) << "\n"; // 输出: 28
cout << sol.uniquePaths(3, 3) << "\n"; // 输出: 6
return 0;
}
JavaScript 完整代码
js
/**
* @param {number} m
* @param {number} n
* @return {number}
*/
var uniquePaths = function(m, n) {
// 使用组合数学方法(更高效)
let ans = 1;
// 确保循环次数最少
if (m > n) [m, n] = [n, m];
for (let y = 1; y < m; y++) {
ans = ans * (n - 1 + y) / y;
}
return ans;
};
// 测试用例
console.log(uniquePaths(3, 7)); // 28
console.log(uniquePaths(3, 2)); // 3
console.log(uniquePaths(7, 3)); // 28
console.log(uniquePaths(3, 3)); // 6
💡 JS 注意:虽然 JS 有 Number 精度限制,但题目保证答案 ≤ 2×10⁹,在安全整数范围内(< 2⁵³),因此无需 BigInt。
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!