【力扣100题】51.不同路径

题目描述

一个机器人位于一个 m x n 网格的左上角(起始点标记为 "Start")。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(标记为 "Finish")。问总共有多少条不同的路径?

示例

复制代码
示例 1:
输入:m = 3, n = 7    输出:28

示例 2:
输入:m = 3, n = 2    输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

示例 3:
输入:m = 7, n = 3    输出:28

示例 4:
输入:m = 3, n = 3    输出:6

约束:1 <= m, n <= 100
答案保证小于等于 2 * 10^9

解题思路总览

方法 核心思想 时间复杂度 空间复杂度 备注
暴力搜索 递归搜索所有可能的路径 O(2^(m+n)) O(m+n) 会超时
记忆化搜索 暴力 + 剪枝(备忘录) O(m*n) O(m*n) 避免重复计算
动态规划 状态转移方程 O(m*n) O(m*n) 最常用
空间优化 一维数组滚动 O(m*n) O(n) 面试最优
数学公式 组合数 C(m+n-2, m-1) O(min(m,n)) O(1) 理论最优

一、暴力搜索(会超时,仅作思路参考)

思路

机器人只能向右或向下,所以到达终点的问题可以分解为:

  • 先向右走一步 + 递归解决子问题
  • 或先向下走一步 + 递归解决子问题
cpp 复制代码
class Solution {
public:
    int uniquePaths(int m, int n) {
        // 从起点 (0,0) 到终点 (m-1, n-1)
        return dfs(0, 0, m, n);
    }

private:
    int dfs(int i, int j, int m, int n) {
        // 到达终点,计数 +1
        if (i == m - 1 && j == n - 1) return 1;

        int ans = 0;
        // 只能向右走
        if (i < m - 1) ans += dfs(i + 1, j, m, n);
        // 只能向下走
        if (j < n - 1) ans += dfs(i, j + 1, m, n);

        return ans;
    }
};

问题分析

复制代码
假设 m=3, n=3,路径展开是一棵二叉树:

                (0,0)
               /     \
           (1,0)     (0,1)
           /   \     /   \
       (2,0) (1,1) (1,1) (0,2)
          \    /\   /\   /
           \  /  \ /  \
            ...  ...  ...

从 (0,0) 到 (m-1,n-1) 最多走 (m-1)+(n-1) = m+n-2 步。每一步都有两种选择,所以总路径数是 2^(m+n-2),当 m=n=100 时,这个数字是 2^198,溢出。

时间复杂度:O(2^(m+n)),空间复杂度:O(m+n)


二、记忆化搜索(递归 + 备忘录)

思路

暴力搜索中存在大量重复计算。例如:

  • 从 (0,0) 到 (2,2) 有多条路径经过 (1,1)
  • (1,1) 被计算了多次,浪费

用 memo 数组记录每个格子到终点的路径数,避免重复计算。

cpp 复制代码
class Solution {
public:
    int uniquePaths(int m, int n) {
        // memo[i][j] 记录从 (i,j) 到终点的路径数
        vector<vector<int>> memo(m, vector<int>(n, -1));
        return dfs(0, 0, m, n, memo);
    }

private:
    int dfs(int i, int j, int m, int n, vector<vector<int>>& memo) {
        // 到达终点
        if (i == m - 1 && j == n - 1) return 1;

        // 之前计算过,直接返回
        if (memo[i][j] != -1) return memo[i][j];

        int ans = 0;
        if (i < m - 1) ans += dfs(i + 1, j, m, n, memo);
        if (j < n - 1) ans += dfs(i, j + 1, m, n, memo);

        memo[i][j] = ans;  // 记忆化
        return ans;
    }
};

图解记忆化

复制代码
以 m=3, n=3 为例:

memo 初始化为 -1(表示未计算):

     j:0   1   2
    +---+---+---+
i:0 | -1 | -1 | -1 |
    +---+---+---+
i:1 | -1 | -1 | -1 |
    +---+---+---+
i:2 | -1 | -1 |  1 |  <- 终点 memo[2][2] = 1
    +---+---+---+

递归计算过程:
dfs(0,0) -> dfs(1,0) -> dfs(2,0) -> dfs(2,1) -> dfs(2,2) = 1
                               -> dfs(1,1) -> ... -> memo[1][1] = 2
                -> dfs(1,1) -> memo[1][1] 已计算 = 2
       -> dfs(0,1) -> ... -> memo[0][1] = 3
memo[0][0] = memo[1][0] + memo[0][1] = 3 + 3 = 6

最终 memo:
     j:0   1   2
    +---+---+---+
i:0 |  6 |  3 | 1 |
    +---+---+---+
i:1 |  3 |  2 | 1 |
    +---+---+---+
i:2 |  1 |  1 | 1 |
    +---+---+---+

时间复杂度:O(m*n),每个格子最多计算一次
空间复杂度:O(m*n)


三、动态规划(最常用)

思路

将记忆化搜索改写为自底向上 的填表方式。

定义 dp[i][j] = 从 (0,0) 到 (i,j) 的不同路径数。

状态转移方程

  • 机器人只能从上方 (i-1,j) 或左方 (i,j-1) 到达 (i,j)
  • dp[i][j] = dp[i-1][j] + dp[i][j-1]

边界条件

  • 第一列 dp[i][0] = 1(只能从上往下走)
  • 第一行 dp[0][j] = 1(只能从左往右走)

完整代码

cpp 复制代码
class Solution {
public:
    int uniquePaths(int m, int n) {
        // 定义二维 dp 数组
        vector<vector<int>> dp(m, vector<int>(n, 0));

        // 初始化:第一列全部为 1(只能从上往下走)
        for (int i = 0; i < m; i++) {
            dp[i][0] = 1;
        }

        // 初始化:第一行全部为 1(只能从左往右走)
        for (int j = 1; j < n; j++) {
            dp[0][j] = 1;
        }

        // 填表:对于其他位置,到达它的路径数 = 从上方来的 + 从左边来的
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }

        return dp[m - 1][n - 1];
    }
};

算法流程图

复制代码
以 m = 3, n = 7 为例:

Step 1: 初始化 dp 数组(全部置 0)

     j:0   1   2   3   4   5   6
    +---+---+---+---+---+---+---+---+
i:0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
    +---+---+---+---+---+---+---+---+
i:1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
    +---+---+---+---+---+---+---+---+
i:2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
    +---+---+---+---+---+---+---+---+

Step 2: 初始化第一列 dp[i][0] = 1(只能从上往下走)

     j:0   1   2   3   4   5   6
    +---+---+---+---+---+---+---+---+
i:0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
    +---+---+---+---+---+---+---+---+
i:1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
    +---+---+---+---+---+---+---+---+
i:2 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
    +---+---+---+---+---+---+---+---+

Step 3: 初始化第一行 dp[0][j] = 1(只能从左往右走)

     j:0   1   2   3   4   5   6
    +---+---+---+---+---+---+---+---+
i:0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
    +---+---+---+---+---+---+---+---+
i:1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
    +---+---+---+---+---+---+---+---+
i:2 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
    +---+---+---+---+---+---+---+---+

Step 4: 按行填表 dp[i][j] = dp[i-1][j] + dp[i][j-1]

     j:0   1   2   3   4   5   6
    +---+---+---+---+---+---+---+---+
i:0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |  <- 第一行 (j=0 已初始化)
    +---+---+---+---+---+---+---+---+
i:1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |   |  <- dp[1][1] = 1+1 = 2
    +---+---+---+---+---+---+---+---+     dp[1][2] = 1+2 = 3
i:2 | 1 | 3 | 6 |10 |15 |21 |28 |   |  <- dp[2][6] = 7+21 = 28
    +---+---+---+---+---+---+---+---+
                                              答案
                                              ====>>> 28

四、空间优化(面试最优解)

思路

观察 DP 表可以发现,第 i 行只依赖第 i-1 行。所以不需要完整的二维数组,一维就够了。

cpp 复制代码
class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<int> dp(n, 1);  // 初始化为 1,第一行全是 1

        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                // dp[j]     : 上一行的值(上方格子)
                // dp[j-1]   : 本行已更新的值(左边格子)
                dp[j] += dp[j - 1];
            }
        }

        return dp[n - 1];
    }
};

图解空间优化

复制代码
初始 dp = [1, 1, 1, 1, 1, 1, 1]  (n=7)

处理 i=1(第二行):
  j=1: dp[1] = dp[1] + dp[0] = 1 + 1 = 2
  j=2: dp[2] = dp[2] + dp[1] = 1 + 2 = 3
  j=3: dp[3] = dp[3] + dp[2] = 1 + 3 = 4
  ...
  dp = [1, 2, 3, 4, 5, 6, 7]

处理 i=2(第三行):
  j=1: dp[1] = dp[1] + dp[0] = 2 + 1 = 3
  j=2: dp[2] = dp[2] + dp[1] = 3 + 3 = 6
  j=3: dp[3] = dp[3] + dp[2] = 4 + 6 = 10
  ...
  dp = [1, 3, 6, 10, 15, 21, 28]

答案:dp[6] = 28  OK

时间复杂度:O(m*n)
空间复杂度:O(n)


五、数学公式(组合数)

思路

机器人从左上角到右下角,总共走 (m-1)+(n-1) = m+n-2 步。

其中,必须走 m-1 步向下,n-1 步向右(顺序可任意)。

所以答案 = 从 m+n-2 步中选 m-1 步向下的组合数:

复制代码
C(m+n-2, m-1) = (m+n-2)! / ((m-1)! * (n-1)!)

代码实现

cpp 复制代码
class Solution {
public:
    int uniquePaths(int m, int n) {
        // 需要计算 C(m+n-2, m-1)
        // 使用 long long 防止溢出
        long long ans = 1;
        int k = min(m - 1, n - 1);  // 取较小的,减少循环次数

        for (int i = 1; i <= k; i++) {
            ans = ans * (n - 1 + i) / i;
        }

        return (int)ans;
    }
};

验证示例

复制代码
m=3, n=7:
C(3+7-2, 3-1) = C(8, 2) = 8*7/2 = 28  OK

m=3, n=2:
C(3+2-2, 3-1) = C(3, 2) = 3*2/2 = 3  OK

m=3, n=3:
C(3+3-2, 3-1) = C(4, 2) = 4*3/2 = 6  OK

逐行解析(对照原题代码)

cpp 复制代码
class Solution {
public:
    int uniquePaths(int m, int n) {
        // 创建 m 行 n 列的二维数组,初始值全为 0
        // dp[i][j] 表示从起点到达 (i, j) 的不同路径数
        vector<vector<int>> dp(m, vector<int>(n, 0));

        // 初始化第一列:对于第一列的每个位置,只能从上方下来
        // 所以 dp[i][0] = 1 (i = 0, 1, 2, ..., m-1)
        for (int i = 0; i < m; i++)
            dp[i][0] = 1;

        // 初始化第一行:对于第一行的每个位置,只能从左边过来
        // 所以 dp[0][j] = 1 (j = 1, 2, ..., n-1)
        // 注意:j 从 1 开始,因为 dp[0][0] 已在前面的循环中设置
        for (int i = 1; i < n; i++)
            dp[0][i] = 1;

        // 填表:对于其他位置 (i, j),机器人可以从上方 (i-1, j) 或
        // 左边 (i, j-1) 到达,所以路径数是两者之和
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }

        // 返回终点的路径数
        return dp[m - 1][n - 1];
    }
};

复杂度分析总结

方法 时间复杂度 空间复杂度 评价
暴力搜索 O(2^(m+n)) O(m+n) 会超时,不可取
记忆化搜索 O(m*n) O(m*n) 避免重复计算
动态规划 O(m*n) O(m*n) 最常用,清晰
空间优化 O(m*n) O(n) 面试最优
组合数 O(min(m,n)) O(1) 数学 trick

面试追问 FAQ

问题 回答要点
Q: 为什么可以用动态规划? 因为机器人只能向右或向下移动,每个位置的路径数只依赖于上方和左方的位置,满足最优子结构和无后效性
Q: 空间还能进一步优化吗? 可以,从一维数组优化到只用一个变量(需数学推导),但面试中 O(n) 已足够
Q: 如果有障碍物怎么办? 在初始化和状态转移时,如果该位置有障碍,直接置 dp[i][j] = 0,表示不可达
Q: 组合数解法的原理? 机器人共走 m+n-2 步,只需选 m-1 步向下(或 n-1 步向右),答案是两者的组合数
Q: 结果会溢出吗? 题目保证答案 <= 210^9,在 int 范围内(2^31-1 约等于 2.110^9),但计算过程中可能超出,需用 long long
Q: 能否用 BFS? BFS 也可以,从起点开始向四周扩展,遇到终点就计数,但需要维护访问集合,复杂度和 DP 相当

相关题目

题目编号 题目名称 难度 核心差异
62 不同路径 中等 基础题,无障碍
63 不同路径 II 中等 有障碍物,障碍处 dp = 0
64 最小路径和 中等 不仅计数,还要求路径和最小
120 三角形最小路径和 中等 三角形网格,从顶到底
174 地下城游戏 困难 从右下向左上 DP,求最小生命值
931 下降路径最小和 中等 矩阵,可以向左下/正下/右下走

总结

要点 内容
核心思想 动态规划,分解子问题,每个位置由上方和左方转移而来
状态定义 dpij = 到达位置 (i, j) 的不同路径数
转移方程 dpij = dpi-1j + dpij-1
初始化 第一行和第一列全部初始化为 1
边界条件 机器人只能向右或向下,不会回退
空间优化 滚动数组,从二维 O(m*n) 优化到一维 O(n)

这是一道经典的动态规划入门题,掌握后可以轻松扩展到带障碍物、带权重、路径求和等变种问题。面试中推荐使用空间优化版,既高效又体现对算法的理解深度。


相关推荐
JAVA面经实录91720 小时前
Java 数据结构与算法 (终极完整学习文档)
java·数据结构·算法
程序员三藏21 小时前
Web自动化测试详解
自动化测试·软件测试·python·selenium·测试工具·职场和发展·测试用例
开源Z1 天前
LeetCode 42 · 接雨水:从暴力到双指针的三步优化
算法·leetcode
旖-旎1 天前
《LeetCode 695 岛屿的最大面积 FloodFill DFS 解法》
c++·算法·力扣·深度优先遍历·floodfill
syagain_zsx1 天前
STL 之 vector 讲练结合
c++·算法
MartinYeung51 天前
[论文学习]DP2Unlearning:高效且具保证的大型语言模型遗忘框架(基于差分隐私的 LLM Unlearning 方法)
学习·算法·语言模型
Tian_Hang1 天前
C++原型模式(Protype)
开发语言·c++·算法
bIo7lyA8v1 天前
算法复杂度的渐进分析与实际运行时间的差异的技术8
算法
yuan199971 天前
欧拉梁静力与屈曲计算的 MATLAB 实现(有限差分法 + 解析解)
开发语言·算法·matlab