前言
动态规划专题,从简到难通关动态规划。
每日一题
今天的题目是 63. 不同路径 II,难度为中等
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
示例 1:
输入: obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出: 2
解释: 3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
示例 2:
输入: obstacleGrid = [[0,1],[0,0]]
输出: 1
提示:
m == obstacleGrid.lengthn == obstacleGrid[i].length1 <= m, n <= 100obstacleGrid[i][j]为0或1
题解
首先机器人每次的移动方向只能是向右或者向下,那么每一个点都可以分出两个不同的选择,走过的路径就可以用一颗二叉树来表示,值得注意的是,因为存在障碍物,所以在某一个节点,如果向右或者向下碰到的是障碍物的话,那么二叉树的这个分支到这里就应该完结了,并且这个不能算是一个有效分支。
当然也可以反向思考,终点一定是题目给的 最右下角,这里假定为 [m,n] 那么我们从这里一步一步往后退,要是碰到了障碍物的话,说明正常的路径也是会碰到这个障碍物的,那么结局是一样的
深度搜索
既然我们能够把路径转化为一颗树,那么只需要对这棵树进行深度遍历,找到所以到达叶子节点的方式,就能够得到题目需要的答案,我这里是使用的第二种图,从终点退回来的树图。
function uniquePathsWithObstacles(obstacleGrid) {
if(!obstacleGrid.length) return 1
const n = obstacleGrid.length
const m = obstacleGrid[0].length
const dfs = (x,y) => {
if(x<1||y<1) {
return 0
}
if(obstacleGrid[x-1][y-1]) {
return 0
}
if(x==1 && y==1) {
return 1
}
return dfs(x-1, y) + dfs(x, y-1)
}
return dfs(n,m)
};
// console.log(uniquePathsWithObstacles([[0,0,0],[0,1,0],[0,0,0]]));
// 2
当然需要判断一下,退后不能够退出整个图,也就是 m, n 都不能够小于1 ,并且图中存在障碍物,假设说下一次向左或者像上碰到了障碍物,那么说明此路不通,也要返回 0
就拿题目这张图举例, m = 7 , n = 3 那么就不只是需要判断下次向左或者向上会不会超出边界,还需要判断会不会碰到障碍物。
但是当我们拿着这个递归代码去提交,就会发现超时了
因为递归一个二叉树,他的时间复杂度是 O(2^n) 级别的,是属于指数增长的,所以当 m, n 都比较大的时候,就会导致超时。
看图能够发现,其实机器人走过的很多路都是重复的,比方说 从 [m, n] 到达 [m-1, n] 或者 [m, n-1] 之后两个点都可能去到 [m-1, n-1] 这就会导致重复计算
所以我们还是需要动态规划的思路,利用 dp 数组来保存每一个点的状态
动态规划
利用动态规划的五部曲来操作
-
确定 dp 数组以及下标的含义,根据我们上面的分析,由于同一个点可能有不同的来源,所以我们要记录的是每一个点位的不同路径,dp[x][y] 代表的是 从 [m, n] 出发,到达[1,] 有几条不同的路径。
-
确定递归公式,想要到达最后的 [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 = 3 , n = 3 ,[1,1] 为障碍物为例 | 1 | 1 | 1 | | - | - | - | | 1 | 0 | 1 | | 1 | 1 | 2
根据上面深度搜索的代码利用dp数组进行优化得到
function uniquePathsWithObstacles(obstacleGrid) {
if(!obstacleGrid.length) return 1
const m = obstacleGrid.length
const n = obstacleGrid[0].length
let dp = new Array(m).fill(0).map(e=>new Array(n).fill(0))
const dfs = (x,y) => {
if(x<1||y<1) {
return 0
}
if(obstacleGrid[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 为 题目 中 数组长度 的大小