【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!💪

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

相关推荐
KaneLogger9 小时前
【Agent】openclaw + opencode 打造助手 安装篇
人工智能·google·程序员
sunny_9 小时前
面试踩大坑!同一段 Node.js 代码,CJS 和 ESM 的执行顺序居然是反的?!99% 的人都答错了
前端·面试·node.js
ayqy贾杰11 小时前
Agent First Engineering
前端·vue.js·面试
唐叔在学习12 小时前
就算没有服务器,我照样能够同步数据
后端·python·程序员
Lee川14 小时前
解锁 JavaScript 的灵魂:深入浅出原型与原型链
javascript·面试
swipe14 小时前
从原理到手写:彻底吃透 call / apply / bind 与 arguments 的底层逻辑
前端·javascript·面试
CoovallyAIHub15 小时前
Moonshine:比 Whisper 快 100 倍的端侧语音识别神器,Star 6.6K!
深度学习·算法·计算机视觉
CoovallyAIHub16 小时前
速度暴涨10倍、成本暴降6倍!Mercury 2用扩散取代自回归,重新定义LLM推理速度
深度学习·算法·计算机视觉
CoovallyAIHub16 小时前
实时视觉AI智能体框架来了!Vision Agents 狂揽7K Star,延迟低至30ms,YOLO+Gemini实时联动!
算法·架构·github