动态规划-62.不同路径 II

215 阅读1分钟

4月日新计划更文活动 第3天

前言

动态规划专题,从简到难通关动态规划。

每日一题

今天的题目是 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.length
  • n == obstacleGrid[i].length
  • 1 <= m, n <= 100
  • obstacleGrid[i][j] 为 0 或 1

题解

首先机器人每次的移动方向只能是向右或者向下,那么每一个点都可以分出两个不同的选择,走过的路径就可以用一颗二叉树来表示,值得注意的是,因为存在障碍物,所以在某一个节点,如果向右或者向下碰到的是障碍物的话,那么二叉树的这个分支到这里就应该完结了,并且这个不能算是一个有效分支。

image.png

当然也可以反向思考,终点一定是题目给的 最右下角,这里假定为 [m,n] 那么我们从这里一步一步往后退,要是碰到了障碍物的话,说明正常的路径也是会碰到这个障碍物的,那么结局是一样的

image.png

深度搜索

既然我们能够把路径转化为一颗树,那么只需要对这棵树进行深度遍历,找到所以到达叶子节点的方式,就能够得到题目需要的答案,我这里是使用的第二种图,从终点退回来的树图。

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

image.png

就拿题目这张图举例, m = 7 , n = 3 那么就不只是需要判断下次向左或者向上会不会超出边界,还需要判断会不会碰到障碍物。

但是当我们拿着这个递归代码去提交,就会发现超时了

image.png

因为递归一个二叉树,他的时间复杂度是 O(2^n) 级别的,是属于指数增长的,所以当 m, n 都比较大的时候,就会导致超时。

看图能够发现,其实机器人走过的很多路都是重复的,比方说 从 [m, n] 到达 [m-1, n] 或者 [m, n-1] 之后两个点都可能去到 [m-1, n-1] 这就会导致重复计算

所以我们还是需要动态规划的思路,利用 dp 数组来保存每一个点的状态

动态规划

利用动态规划的五部曲来操作

  1. 确定 dp 数组以及下标的含义,根据我们上面的分析,由于同一个点可能有不同的来源,所以我们要记录的是每一个点位的不同路径,dp[x][y] 代表的是 从 [m, n] 出发,到达[1,] 有几条不同的路径。

  2. 确定递归公式,想要到达最后的 [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] 的方式的和。

  3. dp数组初始化,首先为了避免在遍历的过程当中下标不存在,我们可以先初始化一个二维数组,大小为 [m][n] 并且将 [1,1] 的位置初始化为 1

  4. 确定遍历顺序,这点就根据上面的说明,可以选择两种遍历方式,一种是从 [1,1] 前进,一种是从 [m,n] 后退,最后结果都是相同的,我这里使用的是 从 [m,n] 后退的方式。

  5. 推导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 为 题目 中 数组长度 的大小

image.png