【LeetCode Hot100 刷题日记 (91/100)】62. 不同路径 —— 动态规划、组合数学、数学 🧮

📌 题目链接: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,由于整数除法截断,会导致错误!


🧭 解题思路(分步拆解)

  1. 理解移动规则:只能右或下 → 路径无回头,问题具有无后效性。
  2. 识别子问题 :到达 (i,j) 的路径数 = 到达 (i-1,j) + (i,j-1)
  3. 确定边界:第一行/列只有 1 条路径。
  4. 选择方法
    • 若需展示 DP 思维 → 用二维 DP;
    • 若要最优性能 → 用组合数学;
    • 若面试官追问空间优化 → 提出滚动数组。
  5. 验证小例子 :如 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!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!

相关推荐
mCell9 小时前
如何零成本搭建个人站点
前端·程序员·github
寻寻觅觅☆10 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
m0_6070766010 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
偷吃的耗子11 小时前
【CNN算法理解】:三、AlexNet 训练模块(附代码)
深度学习·算法·cnn
NEXT0611 小时前
二叉搜索树(BST)
前端·数据结构·面试
化学在逃硬闯CS11 小时前
Leetcode1382. 将二叉搜索树变平衡
数据结构·算法
NEXT0611 小时前
JavaScript进阶:深度剖析函数柯里化及其在面试中的底层逻辑
前端·javascript·面试
ceclar12312 小时前
C++使用format
开发语言·c++·算法
Gofarlic_OMS12 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
夏鹏今天学习了吗12 小时前
【LeetCode热题100(100/100)】数据流的中位数
算法·leetcode·职场和发展