题目来源
题目描述
题目解析
从暴力搜索到动态规划
暴力搜索
class Solution {
// 机器人从[i, j]走到[m, n],一共有几种方法
int process(int i, int j, int m, int n){
if(i == m && j == n){
return 1; // 已经到了目的地
}else if(i == m){ // 此时只能往右
return process(i, j + 1, m, n);
}else if(j == n){ // 此时只能往
return process(i + 1, j, m, n);
}else{
return process(i, j + 1, m, n) + process(i + 1, j, m, n);
}
}
public:
int uniquePaths(int m, int n) {
return process(1, 1, m, n);
}
};
暴力搜索改动态规划
(1)准备表,分析递归函数的可变参数个数与变化范围
int process(int i, int j, int m, int n)
- i,变化范围为[1, m]
- j,变化范围为[1, n]
有两个参数就准备一个二维数组,如下:
std::vector<std::vector<int>> dp(m + 1, std::vector<int>(n + 1, 0));
(2)返回值。分析主函数是怎么调用递归函数的
return process(1, 1, m, n);
所以应该返回dp[1][1]
(3)填表
- 先分析base case
if(i == m && j == n){
return 1; // 已经到了目的地
}
- 所以初始化dp[m][n] = 1
- 在分析普通情况
- 可以看出dp[i][j],依赖dp[i][j+1]、dp[i+1][j]、dp[i+1][j+1],所以应该从下到上,从右往左填
else if(i == m){ // 此时只能往右
return process(i, j + 1, m, n);
}else if(j == n){ // 此时只能往
return process(i + 1, j, m, n);
}else{
return process(i, j + 1, m, n) + process(i + 1, j, m, n);
}
(4)综上,代码如下:
class Solution {
public:
int uniquePaths(int m, int n) {
if(m == 1 && n == 1){
return 1;
}
std::vector<std::vector<int>> dp(m + 1, std::vector<int>(n + 1, 0));
dp[m][n] = 1;
for (int j = n - 1; j >= 1; --j) {
dp[m][j] = dp[m][j + 1];
}
for (int i = m - 1; i >= 1; --i) {
dp[i][n] = dp[i + 1][n];
}
for (int i = m - 1; i >= 1; --i) {
for (int j = n - 1; j >= 1; --j) {
dp[i][j] = dp[i][j + 1] + dp[i + 1][j];
}
}
return dp[1][1];
}
};
深度优先搜索
因为机器人只能往下或者向右移动一步,所以机器人走过的路径可以抽象成一颗二叉树,而叶子节点就是终点
此时问题可以转换为求二叉树子节点的个数
class Solution {
private:
int dfs(int i, int j, int m, int n) {
if (i > m || j > n) return 0; // 越界了
if (i == m && j == n) return 1; // 找到一种方法,相当于找到了叶子节点
return dfs(i + 1, j, m, n) + dfs(i, j + 1, m, n);
}
public:
int uniquePaths(int m, int n) {
return dfs(1, 1, m, n);
}
};
来分析一下时间复杂度,这个深搜的算法,其实就是要遍历整个二叉树。
这棵树的深度其实就是m+n-1(深度按从1开始计算)。
那二叉树的节点个数就是 2^(m + n - 1) - 1。可以理解深搜的算法就是遍历了整个满二叉树(其实没有遍历整个满二叉树,只是近似而已)
所以上面深搜代码的时间复杂度为O(2^(m + n - 1) - 1),可以看出,这是指数级别的时间复杂度,是非常大的。
排列组合
-
C
(
m
+
n
−
2
,
m
i
n
(
m
−
1
,
n
−
1
)
)
C(m+n-2,min(m-1,n-1))
C(m+n−2,min(m−1,n−1));
class Solution {
// 调用的时候,请保证初次调用时,m和n都不为0
long gcd(long m, long n) {
return n == 0 ? m : gcd(n, m % n);
}
public:
int uniquePaths(int m, int n) {
int right = n - 1;
int all = m + n - 2;
long o1 = 1;
long o2 = 1;
// o1乘进去的个数 一定等于 o2乘进去的个数
for (int i = right + 1, j = 1; i <= all; i++, j++) {
o1 *= i;
o2 *= j;
long g = gcd(o1, o2);
o1 /= g;
o2 /= g;
}
return (int) o1;
}
};
类似题目
题目 | 思路 |
---|---|
leetcode:62. 走到右下角有多少条路径 Unique Paths | 机器人从[i, j]走到[m, n],一共有几种方法:先判断是不是到了目的地,如果是,那么找到了一种方法;判断是不是只能往右走;判断是不是只能往下走;否则往下+往右(之所以不检查边界是因为已经确保不超过边界)) |
leetcode:63. 走到右下角有多少条不同路径(可能有障碍) Unique Paths II | 机器人从[i, j]走到[m, n],一共有几种方法:先边界/障碍物检查,如果是,返回0;再终点检查,如果是,返回1;否则往下+往右 |
leetcode:64. 走到右下角最小路径和 Minimum Path Sum | 从终点往前面看,定义从左上角到坐标(i, j)的最短路径和:如果当前是起点,那么直接返回grid[0][0];否则判断是不是第一行,第一行只能从左边走过来 + grid[i][j];再判断是不是第一列,第一列只能从上面走下来 + grid[i][j];普通情况是:取从上面走下来和从左边走过来的最小值+当前坐标的值grid[i][j] |
leetcode:174. 骑士救公主需要的最小血量 Dungeon Game | 逆序动态规划: 令dp[i][j]表示从坐标[i,j]到达终点所需要的最小初始血量。 |
leetcode:120. 三角形最小路径和 | |
leetcode:980. 从起点到终点一共有多少条路径(有障碍,四个方向可以走,只能走一次,所有的可用格子要用完) III | 因为四个方向都可以走,所以必须回溯 |
leetcode:741. 最多能摘多少樱桃樱桃 Cherry Pickup | 一个人来回走等价成两个人从起点走到终点。也就是两个人A、B都从左下角走到右下角,都只能向下或者向右走,但是A和B能做出不同的选择。如果,某一时刻,AB进入相同的格子,A和B就只能获得一份。如果某一个位置,A也来过,B也来过,AB一定是同时来的,而不会分先后,因为AB同时走 |
1463. 摘樱桃 II |