(LeetCodeHot100)62. 不同路径——unique-paths

62. 不同路径——unique-paths

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 1:

img

1
2
输入:m = 3, n = 7
输出:28

示例 2:

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

示例 3:

1
2
输入:m = 7, n = 3
输出:28

示例 4:

1
2
输入:m = 3, n = 3
输出:6

提示:

  • 1 <= m, n <= 100
  • 题目数据保证答案小于等于 2 * 109

方法一:动态规划

思路与算法

我们用 f(i,j) 表示从左上角走到 (i,j) 的路径数量,其中 ij 的范围分别是 [0,m) 和 [0,n)。

由于我们每一步只能从向下或者向右移动一步,因此要想走到 (i,j),如果向下走一步,那么会从 (i−1,j) 走过来;如果向右走一步,那么会从 (i,j−1) 走过来。因此我们可以写出动态规划转移方程:

f(i,j)=f(i−1,j)+f(i,j−1)

需要注意的是,如果 i=0,那么 f(i−1,j) 并不是一个满足要求的状态,我们需要忽略这一项;同理,如果 j=0,那么 f(i,j−1) 并不是一个满足要求的状态,我们需要忽略这一项。

初始条件为 f(0,0)=1,即从左上角走到左上角有一种方法。

最终的答案即为 f(m−1,n−1)。

细节

为了方便代码编写,我们可以将所有的 f(0,j) 以及 f(i,0) 都设置为边界条件,它们的值均为 1。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int uniquePaths(int m, int n) {
int[][] f = new int[m][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];
}
}

此外,由于 f(i,j) 仅与第 i 行和第 i−1 行的状态有关,因此我们可以使用滚动数组代替代码中的二维数组,使空间复杂度降低为 O(n)。( “当前状态只依赖上一行” 这个本质 —— 滚动数组通过「覆盖旧值」的方式,用一维数组复用空间,f[j] 在更新前恰好保存着上一行的对应值)

因为一维数组的更新顺序是 从左到右(j从1到n-1,且我们用「覆盖旧值」的方式复用空间:

  1. 在计算第 i 行的 j 列之前,f[j] 还没被修改,保存的是第 i-1j 列的值(即 f[i-1][j],“上方” 的路径数);
  2. 计算时,f[j-1] 已经被修改过(因为 j-1 < j,先遍历 j-1),保存的是第 ij-1 列的值(即 f[i][j-1],“左方” 的路径数);
  3. 执行 f[j] += f[j-1] 后,f[j] 就从 “上一行的值” 变成了 “当前行的值”,供下一列(j+1)使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public int uniquePaths(int m, int n) {
int[] f = new int[n];
for (int i = 0; i < n; ++i) {
f[i] = 1;
}
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
f[j] += f[j - 1];
}
}
return f[n - 1];
}
}

复杂度分析

  • 时间复杂度:O(mn)。
  • 空间复杂度:O(min(m,n)),即为存储所有状态需要的空间。注意到 f(i,j) 仅与第 i 行和第 i−1 行的状态有关,因此我们可以使用滚动数组代替代码中的二维数组,使空间复杂度降低为 O(n)。此外,由于我们交换行列的值并不会对答案产生影响,因此我们总可以通过交换 mn 使得 mn,这样空间复杂度降低至 O(min(m,n))。

方法二:组合数学

思路与算法

从左上角到右下角的过程中,我们需要移动 m+n−2 次,其中有 m−1 次向下移动,n−1 次向右移动。因此路径的总数,就等于从 m+n−2 次移动中选择 m−1 次向下移动的方案数,即组合数:

$\begin{equation} C_{m+n-2}^{m-1} = \binom{m+n-2}{m-1} = \frac{(m + n - 2)(m + n - 3) \cdots n}{(m - 1)!} = \frac{(m + n - 2)!}{(m - 1)!(n - 1)!} \end{equation}$

因此我们直接计算出这个组合数即可。计算的方法有很多种:

  • 如果使用的语言有组合数计算的 API,我们可以调用 API 计算;
  • 如果没有相应的 API,我们可以使用 (m−1)!(m+n−2)(m+n−3)⋯n 进行计算。

代码

1
2
3
4
5
6
7
8
9
class Solution {
public int uniquePaths(int m, int n) {
long ans = 1;
for (int x = n, y = 1; y < m; ++x, ++y) {
ans = ans * x / y;
}
return (int) ans;
}
}

复杂度分析

  • 时间复杂度:O(m)。由于我们交换行列的值并不会对答案产生影响,因此我们总可以通过交换 mn 使得 mn,这样时间复杂度降低至 O(min(m,n))。
  • 空间复杂度:O(1)。