62. Unique Paths(不同路径)
1. 题目描述
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
例如,上图是一个7 x 3 的网格。有多少可能的路径?
示例 1:
输入: m = 3, n = 2
输出: 3
解释: 从左上角开始,总共有 3 条路径可以到达右下角。
- 向右 -> 向右 -> 向下
- 向右 -> 向下 -> 向右
- 向下 -> 向右 -> 向右
示例 2:
输入: m = 7, n = 3
输出: 28
提示:
- 1 <= m, n <= 100
- 题目数据保证答案小于等于 2 * 10 ^ 9
2. 回溯法(Backtracking, Time Limit Exceeded)
2.1 解题思路
因为机器人只能向右和向下走,那么:
- 右走一步:j + 1,或者下走一步:i + 1;如果j越界,则返回上一步,继续i + 1,;如果i越界,则返回上一步;
- 如果i = 终点i且j = 终点j,则路径数加一,返回上一步继续1. 的步骤;
回溯法的思路简单清晰,但是有非常多重复步骤(前面有*),如下图所示:
这种重复计算导致大量数据处理超时,所以接下来的动态规划方法中,我们需要对其进行简化。
2.2 实例代码
2.2.1 不使用面向对象(推荐)
class Solution {
int count = 0;
void move(int i, int j, int iFinish, int jFinish) {
if (i >= iFinish + 1 || j >= jFinish + 1) return;
if (i == iFinish && j == jFinish) { count++; return; }
move(i, j + 1, iFinish, jFinish);
move(i + 1, j, iFinish, jFinish);
}
public:
int uniquePaths(int m, int n) {
move(0, 0, m - 1, n - 1);
return this->count;
}
};
2.2.2 使用面向对象
struct Point {
int x;
int y;
Point(int i, int j):x(i), y(j) {}
bool operator==(Point& p) { return this->x == p.x && this->y == p.y; }
};
class Solution {
int count = 0;
void move(Point& start, Point& finish) {
if (start.x >= finish.x + 1 || start.y >= finish.y + 1) return;
if (start == finish) { count++; return; }
start.x++;
move(start, finish);
start.x--;
start.y++;
move(start, finish);
start.y--;
}
public:
int uniquePaths(int m, int n) {
Point finish(n - 1, m - 1), start(0, 0);
move(start, finish);
return this->count;
}
};
3. 动态规划(Dynamic Programming)
3.1 解题思路
根据2. 回溯法(Backtracking)的分析,我们发现某一点坐标(i, j)一共有多少个路径,取决于它右边的坐标(i, j+1)和下面的坐标(i+1, j)的路径数,即:
dp[i][j] = dp[i + 1][j] + dp[i][j + 1]
所以我们可以用一个二维数组来存储上面重复的步骤,如果已经处理过相同的情况,直接返回存储的数值,避免重复计算 ,适用代码:3.2.1 递归(Recursion)。
既然已经使用了递归,那么能不能使用迭代来解决呢?首先,我们来观察一下每个坐标的路径数,m = n = 3:
- | - | - |
---|---|---|
6 | 3 | 1 |
3 | 2 | 1 |
1 | 1 | 1 |
通过上图,我们发现:右边界(j = n - 1)和下边界(i = m - 1)的路径数总是1,因为右边界的值只取决于它的下坐标值,不能往右走,而下边界的值只取决于它的右坐标值,不能往下走。而其余坐标的值和上面总结出来的公式是一致的,所以我们只需先初始化右边界和下边界的值为1,然后用上面的公式计算中间的坐标的值,最后返回dp[0][0]即可,适用代码:3.2.2 迭代(Iteration)。
最后,我们换一种观察角度,从最大行(i),最大列(j)开始观察,我们发现前一行等于后一行中,本列的值 += 后一列的值,即:
dp[j] = dp[j] + dp[j + 1]
这样就可以把之前使用的二维数组更改成一位数组,以便达到节约空间的效果。如果有童鞋还是不是很能理解上面的思路,可以这样理解:
原来我们是把右坐标值和下坐标值分别存储到对应坐标空间里面(dp[i + 1][j] 和 dp[i][j + 1]),而现在我们把上面两个坐标的值放分别在两个连续的空间中(dp[j] + dp[j + 1]),然后相加以后把新值放在下坐标的空间中(dp[j]),适用代码:3.2.3 滚动优化版迭代(Iteration updated by row)。
3.2 实例代码
3.2.1 递归(Recursion)
class Solution {
int move(int i, int j, int iFinish, int jFinish, vector<vector<int>>& memory) {
if (i >= iFinish + 1 || j >= jFinish + 1) return 0;
if (memory[i][j]) return memory[i][j];
if (i == iFinish && j == jFinish) return 1;
memory[i][j] = move(i, j + 1, iFinish, jFinish, memory) + move(i + 1, j, iFinish, jFinish, memory);
return memory[i][j];
}
public:
int uniquePaths(int m, int n) {
vector<vector<int>> memory(m, vector<int>(n, 0));
memory[0][0] = move(0, 0, m - 1, n - 1, memory); // corner case: m = n = 1
return memory[0][0];
}
};
3.2.2 迭代(Iteration)
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> memory(m, vector<int>(n, 1)); // 这里已经初始化了右边界和下边界
for (int i = m - 2; i >= 0; i--)
for (int j = n - 2; j >= 0; j--)
memory[i][j] = memory[i + 1][j] + memory[i][j + 1];
return memory[0][0];
}
};
3.2.3 滚动优化版迭代(Iteration updated by row)
class Solution {
public:
int uniquePaths(int m, int n) {
vector<int> memory(n, 1);
for (int i = m - 2; i >= 0; i--)
for (int j = n - 2; j >= 0; j--)
memory[j] = memory[j] + memory[j + 1];
return memory[0];
}
};