# 1631. 最小体力消耗路径

<https://leetcode-cn.com/problems/path-with-minimum-effort/>

## 题目描述

```
你准备参加一场远足活动。给你一个二维 rows x columns 的地图 heights ，其中 heights[row][col] 表示格子 (row, col) 的高度。一开始你在最左上角的格子 (0, 0) ，且你希望去最右下角的格子 (rows-1, columns-1) （注意下标从 0 开始编号）。你每次可以往 上，下，左，右 四个方向之一移动，你想要找到耗费 体力 最小的一条路径。

一条路径耗费的 体力值 是路径上相邻格子之间 高度差绝对值 的 最大值 决定的。

请你返回从左上角走到右下角的最小 体力消耗值 。

 

示例 1：

```

![](https://p.ipic.vip/qcib1m.jpg)

```

输入：heights = [[1,2,2],[3,8,2],[5,3,5]]
输出：2
解释：路径 [1,3,5,3,5] 连续格子的差值绝对值最大为 2 。
这条路径比路径 [1,2,2,2,5] 更优，因为另一条路劲差值最大值为 3 。
示例 2：

```

![](https://p.ipic.vip/as0bds.jpg)

```

输入：heights = [[1,2,3],[3,8,4],[5,3,5]]
输出：1
解释：路径 [1,2,3,4,5] 的相邻格子差值绝对值最大为 1 ，比路径 [1,3,5,3,5] 更优。
示例 3：

```

![](https://p.ipic.vip/c6cw0y.jpg)

```

输入：heights = [[1,2,1,1,1],[1,2,1,2,1],[1,2,1,2,1],[1,2,1,2,1],[1,1,1,2,1]]
输出：0
解释：上图所示路径不需要消耗任何体力。
 

提示：

rows == heights.length
columns == heights[i].length
1 <= rows, columns <= 100
1 <= heights[i][j] <= 10e6

```

## 前置知识

* 二维矩阵
* [深度优先遍历](https://github.com/azl397985856/leetcode/blob/master/thinkings/DFS.md)
* [二分查找](https://leetcode-solution-leetcode-pp.gitbook.io/leetcode-solution/91/binary-search)

## 公司

* 暂无

## 思路

如果采用暴力解法， 需要找出所有的路径，然后返回最小代价的即可，时间复杂度是指数级别。回头看一下数据范围是 10e6，因此这种解法是不行的。

由于题目的解空间是 \[0, 10\*\*6 - 1]。

> 对解空间这个概念不熟悉的，可以看我之前的一篇题解[686. 重复叠加字符串匹配](https://leetcode-solution-leetcode-pp.gitbook.io/leetcode-solution/medium/686.repeated-string-match)

本质上，我们需要进行发问：

* 0 可以么？
* 1 可以么？
* 2 可以么？
* 。。。

直到找到第一个不可以的，我们返回前一个即可。

关于可不可以，我们可以使用 DFS 来做，由于只需要找到一条满足条件的，或者找到一个不满足的提前退出，因此最坏的情况是一直符合，并走到终点，这种情况下时间复杂度是 $(m \times n)$，因此总的时间复杂度是 $O(m \times n \times 10\*\*6)$。

实际上，上面的不断发问的过程不就是一个连续的递增序列么？ 我们的目标不就是在一个连续递增序列找指定值么？于是二分法就不难想到。

而且这道题本质就是二分查找中的**查找最右侧满足条件的值**，关于这个问题，我已经在 [【91 天学算法】二分查找](https://leetcode-solution-leetcode-pp.gitbook.io/leetcode-solution/91/binary-search) 中进行了详细描述，并给出了代码模板，直接套就可以了。

值得注意的是，我们只需要找到一个满足条件的路径即可，因此可以利用短路剪枝。

```py
return dfs(i + 1, j, heights[i][j], target) or dfs(i - 1, j, heights[i][j], target) or dfs(i, j + 1, heights[i][j], target) or dfs(i, j - 1, heights[i][j], target)
```

而不是写出下面的代码（下面的代码会超时）:

```py
top = dfs(i + 1, j, heights[i][j], target)
bottom = dfs(i - 1, j, heights[i][j], target)
right = dfs(i, j + 1, heights[i][j], target)
left = dfs(i, j - 1, heights[i][j], target)
return top or bottom or right or left

```

## 代码

代码支持：Python3

```py
class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        lo, hi = 0, 10**6 - 1
        m, n = len(heights), len(heights[0])
        def dfs(i, j, pre, target):
            if (i, j) in visited: return False
            if i < 0 or i >= m or j < 0 or j >= n or abs(heights[i][j] - pre) > target: return False
            if i == m - 1 and j == n - 1: return True
            visited.add((i, j))
            return dfs(i + 1, j, heights[i][j], target) or dfs(i - 1, j, heights[i][j], target) or dfs(i, j + 1, heights[i][j], target) or dfs(i, j - 1, heights[i][j], target)
        # 查找最右侧满足条件的值
        while lo <= hi:
            visited = set()
            mid = (lo + hi) >> 1
            if dfs(0, 0, heights[0][0], mid): hi = mid - 1
            else: lo = mid + 1
        return lo

```

**复杂度分析**

m 为 矩阵的高度， n 为矩阵的长度。

* 时间复杂度：$O(4 \times m \times n \times log\_2 10^6)$，其中 $log\_2 10^6$ 为二分的次数， $4 \times m \times n$ 为每次 dfs 的时间。
* 空间复杂度：$O(m \times n)$，不管是递归的栈开销还是 visited 的开销都是 $O(m \times n)$。

## 相关问题

* [875. 爱吃香蕉的珂珂](https://leetcode-solution-leetcode-pp.gitbook.io/leetcode-solution/medium/875.koko-eating-bananas)
