这是我参与8月更文挑战的第17天,活动详情查看:8月更文挑战
前言
关于 LeetCode 数组类型题目的相关解法,可见LeetCode 数组类型题目做前必看,分类别解法总结了题目,可以用来单项提高。觉得有帮助的话,记得多多点赞关注哦,感谢!
题目描述
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
示例 1:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
- 向右 -> 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右 -> 向右
示例 2:
输入:obstacleGrid = [[0,1],[0,0]]
输出:1
链接:leetcode-cn.com/problems/un…
题解
这道题目采用记忆化递归的方法来做.首先解释一下什么是记忆化递归.
拿经典的递归题目, 斐波那契数列来讲, 斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2), 那么如何求 F(n) 的大小呢? 我们通常的解法就是递归
const f = (n) => {
if (n <= 1) return n
return f(n-1) + f(n-2)
}
这种递归存在的问题是同一个 f(n) 有可能被重复计算, 比如计算 f(10), 那么根据递归公式, f(10) = f(9) + f(8), 而 f(9) = f(8) + f(7), 可以看到在计算 f(10) 和 f(9) 的时候都需要计算 f(8) 的值.
记忆化递归就是用一个数组记录下已经计算好的 f(n) 值, 当下一次用到时, 直接从数组中拿出来, 而不用再次递归的调用, 也算是空间换时间的一种做法. 斐波那契数列问题用记忆化递归的方法来做就是
const m = new Array(n).fill(0)
cosnt f = (n) => {
if (n <= 1) return n
if (!m[n]) {
m[n] = f(n - 1) + f(n - 2)
}
return m[n]
}
上面的解法中, 我们用数组 m 来记录了 f(n) 是否有被计算过, 如果计算过了就直接返回 m[n], 否则就递归计算 m[n] = f(n - 1) + f(n -2)
这种记忆化递归的方法使得算法的时间复杂度变为 O(n), 相应的空间复杂度增加为了 O(n)
那么, 对于本题来讲, 我们也采用记忆化递归的方式来做. 有以下几个要点:
-
新建一个 f 数组, 作为记忆化递归的记录数组, 数组初始化为 -1, 用来判断该位置是否计算过递归的值
-
边界条件是当递归出界时返回 0, 当递归到出发点时返回 1
-
如果遇到了障碍物, 则记忆化递归的数组对应位置值为 0, 否则递归计算
具体的代码见下方, 要注意的点是记忆数组范围是 m + 1 和 n + 1, 所以对应的 obstacleGrid 数组是 i - 1 和 j - 1, 而不是 i, j.
时间复杂度为 O(mn), 空间复杂度为 O(mn)
/**
* @param {number[][]} obstacleGrid
* @return {number}
*/
var uniquePathsWithObstacles = function(obstacleGrid) {
let m = obstacleGrid.length
let n = obstacleGrid[0].length
let f = Array.from({ length: m + 1 }, () => (new Array(n + 1).fill(-1)))
if (obstacleGrid[0][0] === 1) return 0
const paths = (i, j) => {
if (i <= 0 || j <= 0) return 0
if (i === 1 && j === 1) return 1
if (f[i][j] !== -1 ) return f[i][j]
if (obstacleGrid[i-1][j-1] === 1) {
f[i][j] = 0
} else {
f[i][j] = paths(i-1, j) + paths(i, j-1)
}
return f[i][j]
}
return paths(m, n)
};