前言
动态规划专题,从简到难通关动态规划。
每日一题
今天的题目是 62. 不同路径,难度为中等
一个机器人位于一个 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 * 109
题解
由于机器人每次只能向右或者向下,所以走过的路径是可以用一颗二叉树来表示的,比方说起始位置为 [1,1] 那么下一步移动的位置一定是 [1,2] 或者 [2, 1] 只有这两种可能,
当然也可以反向思考,终点一定是题目给的 [m, n] 不存在不能够到达的情况,那么可以从 [m, n] 一步步退回起点
深度搜索
既然我们能够把路径转化为一颗树,那么只需要对这棵树进行深度遍历,找到所以到达叶子节点的方式,就能够得到题目需要的答案,我这里是使用的第二种图,从终点退回来的树图。
function uniquePaths(m, n) {
const dfs = (x,y) => {
if(x<1||y<1) {
return 0
}
if(x==1 && y==1) {
return 1
}
return dfs(x-1, y) + dfs(x, y-1)
}
return dfs(m,n)
};
当然需要判断一下,退后不能够退出整个图,也就是 m, n 都不能够小于1
就拿题目这张图举例, m = 7 , n = 3 那么当向上退了两次以后 m = 7 , n = 1 那么就不能够再往上退后了,也就是这个树已经算是到头了,后面只能够 向左后退,这就是需要判断的边界情况
但是当我们拿着这个递归代码去提交,就会发现超时了
因为递归一个二叉树,他的时间复杂度是 O(2^n) 级别的,是属于指数增长的,所以当 m, n 都比较大的时候,就会导致超时。
看图能够发现,其实机器人走过的很多路都是重复的,比方说 从 [m, n] 到达 [m-1, n] 或者 [m, n-1] 之后两个点都可能去到 [m-1, n-1] 这就会导致重复计算
所以我们还是需要动态规划的思路,利用 dp 数组来保存每一个点的状态
动态规划
利用动态规划的五部曲来操作
-
确定 dp 数组以及下标的含义,根据我们上面的分析,由于同一个点可能有不同的来源,所以我们要记录的是每一个点位的不同路径,dp[x][y] 代表的是 从 [1,1] 出发,到达[x, y] 有几条不同的路径。
-
确定递归公式,想要到达最后的 [m,n] 就只能从 [m-1,n] 或者 [m,n-1] 这两个位置前进,再根据dp数组的含义,dp[m,n] 代表从 [1,1] 出发,到达[m,n] 有几条不同的路径,那么 dp[m,n] 就可以看成 dp[m-1,n] + dp[m,n-1] 。代表到达[m,n] 的方式为 到达[m-1,n]的方式在加上 到达[m,n-1] 的方式的和。
-
dp数组初始化,首先为了避免在遍历的过程当中下标不存在,我们可以先初始化一个二维数组,大小为 [m][n] 并且将 [1,1] 的位置初始化为 1
-
确定遍历顺序,这点就根据上面的说明,可以选择两种遍历方式,一种是从 [1,1] 前进,一种是从 [m,n] 后退,最后结果都是相同的,我这里使用的是 从 [m,n] 后退的方式。
-
推导dp数组,以 m = 7 , n = 3 为例
| 1 | 1 | 1 | 1 | 1 | 1 | 1 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 1 | 3 | 6 | 10 | 15 | 21 | 28 |
根据上面深度搜索的代码利用dp数组进行优化得到
function uniquePaths(m, n) {
let dp = new Array(m).fill(0).map(e=>new Array(n).fill(0))
console.log(dp);
const dfs = (x,y) => {
if(x<1||y<1) {
return 0
}
if(x==1 && y==1) {
return 1
}
if(dp[x-1][y-1]) {
return dp[x-1][y-1]
}else {
let t = dfs(x-1, y) + dfs(x, y-1)
dp[x-1][y-1] = t
return t
}
}
return dfs(m,n)
};
时间复杂度降低到了 O(mn), 空间复杂的 为 O(mn), 其中 m ,n 为 题目 中 m,n 的大小
滚动数组
分析上面 7,3 的这种情况的 dp 数组,我们会发现一个规律
| 1 | 1 | 1 | 1 | 1 | 1 | 1 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 1 | 3 | 6 | 10 | 15 | 21 | 28 |
dp [i,j] 的位置等于 dp[i-1, j] 加上 dp [i, j-1] 并且我们最后要的值一定是 dp[m,n] ,所以其实二位数组显得有些浪费,我们可以只建立一个一维数组,比方说 dp[] 然后初始化长度 等于m ,第一行 一定都是等于 1 的,因为只有一种移动方式,然后向下进行滚动,第二行每个位置的值等于 当前的值加上上一行后一个的值,比方说 2 等于 1 + 1,10 等于 6 + 4,。
那么我们就可以写出这个循环
let dp = new Array(n).fill(1)
for(let j = 1; j < m ; j ++) {
for(let i = 1; i < n; i ++) {
dp[i] = dp[i] + dp[i-1]
}
}
// 当 m = 7, n = 3 dp = [1, 7, 28]
时间复杂度没变,空间复杂度优化到了 O(n) 只需要一个一维数组