LeetCode 1631. 最小体力消耗路径
题目描述




题意分析
从左上角走到右下角,每步可以向上下左右相邻格子移动。单步消耗是两个格子高度差的绝对值,整条路径的体力消耗是所有单步消耗中的最大值。
求能达到的最小路径消耗。这里不累加沿途差值,也不要求步数最少,绕路可能更省力。单格矩阵已经位于终点,没有经过边,答案为零。
解法:Dijkstra 最小化最大边权
核心思路
[!blue]
将格子看成图节点,相邻格子间的边权为高度差。定义
dist[row][col]为目前找到的、到达该格子的最小路径消耗。起点为零,其他格子先设为无穷大。若已用消耗
effort到达当前格子,再经过高度差为step的一条边,新路径消耗是max(effort, step)。它若小于邻格现有记录,就改进邻格距离,并把新记录放入按消耗升序排列的最小堆。普通 Dijkstra 用加法延伸路径,这里改成取最大值仍然成立:延伸路径不会降低已有消耗。每次取出的有效记录是所有待处理候选中最小的;若还存在一条更优路径,它从已确定区域首次走向未确定区域时,就应产生一个更小的候选并优先出堆,矛盾。因此有效出堆时,该格子的消耗已经最优。
同一格子可能先找到较差路线,随后又被改进,旧记录仍会留在堆中。弹出后先与
dist比较,不一致就跳过。首次以有效记录取出终点时可以直接返回,因为此时终点距离已经确定,而不是第一次发现终点或第一次入堆就返回。所有格子均可通行,有限网格中起终点存在连接;搜索会找到最优路径。
解题步骤
- 初始化距离表,起点为零,其余为无穷大;将起点放入最小堆。
- 弹出消耗最小的记录,过期记录直接跳过。
- 若当前格子是终点,返回当前消耗。
- 枚举四个合法邻格,用当前消耗和单步高度差的最大值生成候选。
- 候选更优时更新距离并入堆,继续处理。
代码实现
class Solution {
// 到达某个格子的最优体力值,取决于此前路径体力值与最后一步高度差的较大值。
public int minimumEffortPath(int[][] heights) {
int rows = heights.length;
int cols = heights[0].length;
int[][] dist = new int[rows][cols];
for (int[] row : dist) {
Arrays.fill(row, Integer.MAX_VALUE);
}
PriorityQueue<int[]> minHeap = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
dist[0][0] = 0;
minHeap.offer(new int[] {
0,
0,
0
});
int[][] dirs = {
{1, 0},
{-1, 0},
{0, 1},
{0, -1},
};
while (!minHeap.isEmpty()) {
int[] cur = minHeap.poll();
int effort = cur[0];
int row = cur[1];
int col = cur[2];
// 更好路线已经更新距离,这条堆记录过期。
if (effort != dist[row][col]) {
continue;
}
if (row == rows - 1 && col == cols - 1) {
return effort;
}
for (int[] dir : dirs) {
int nextRow = row + dir[0];
int nextCol = col + dir[1];
if (nextRow < 0 || nextRow >= rows || nextCol < 0 || nextCol >= cols) {
continue;
}
int step = Math.abs(heights[row][col] - heights[nextRow][nextCol]);
// 路径代价取最大的单步高度差,不是沿途求和。
int nextEffort = Math.max(effort, step);
if (nextEffort < dist[nextRow][nextCol]) {
dist[nextRow][nextCol] = nextEffort;
minHeap.offer(new int[] {
nextEffort,
nextRow,
nextCol
});
}
}
}
return 0;
}
}
import "container/heap"
type item struct {
effort int
row int
col int
}
type minHeap []item
func (h minHeap) Len() int {
return len(h)
}
func (h minHeap) Less(i int, j int) bool {
return h[i].effort < h[j].effort
}
func (h minHeap) Swap(i int, j int) {
h[i], h[j] = h[j], h[i]
}
func (h *minHeap) Push(x any) {
*h = append(*h, x.(item))
}
func (h *minHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
func minimumEffortPath(heights [][]int) int {
rows, cols := len(heights), len(heights[0])
dist := make([][]int, rows)
for i := range dist {
dist[i] = make([]int, cols)
for j := range dist[i] {
dist[i][j] = 1 << 30
}
}
h := &minHeap{}
heap.Push(h, item{effort: 0, row: 0, col: 0})
dist[0][0] = 0
dirs := [][]int{
{1, 0},
{-1, 0},
{0, 1},
{0, -1},
}
for h.Len() > 0 {
cur := heap.Pop(h).(item)
// 更好路线已经更新距离,这条堆记录过期。
if cur.effort != dist[cur.row][cur.col] {
continue
}
if cur.row == rows-1 && cur.col == cols-1 {
return cur.effort
}
for _, dir := range dirs {
nextRow := cur.row + dir[0]
nextCol := cur.col + dir[1]
if nextRow < 0 || nextRow >= rows || nextCol < 0 || nextCol >= cols {
continue
}
step := absInt(heights[cur.row][cur.col] - heights[nextRow][nextCol])
// 路径代价取最大的单步高度差,不是沿途求和。
nextEffort := maxInt(cur.effort, step)
if nextEffort < dist[nextRow][nextCol] {
dist[nextRow][nextCol] = nextEffort
heap.Push(h, item{effort: nextEffort, row: nextRow, col: nextCol})
}
}
}
return 0
}
func absInt(x int) int {
if x < 0 {
return -x
}
return x
}
func maxInt(a int, b int) int {
if a > b {
return a
}
return b
}
复杂度分析
- 时间复杂度:$O(mn\log(mn+1))$。网格有 $mn$ 个节点,每格至多四条邻边,松弛与堆操作总规模为线性乘对数。
- 空间复杂度:$O(mn)$,用于距离表和待处理堆记录。
关键点总结
[!green]
- 路径延伸使用最大值,准确表示沿途最困难的一步。
- 延伸不会使路径消耗下降,最小堆出堆的有效记录可以确定最优值。
- 同一位置允许被更优路线更新,过期堆记录随后忽略。
- 终点有效出堆才可结束,首次入堆还不代表最优。
易错点总结
[!yellow]
- 累加单步高度差求的是另一种路径目标,不能替代取最大值。
- 第一次发现某格就永久标记,会阻止之后更好的路线改进它。
- 使用队列按步数层次搜索,不能直接保证这种加权瓶颈代价最小。
- 距离默认零会让所有正消耗候选无法更新,非起点需要初始化为足够大。
- 最短步数或几何距离最短与最小体力没有等价关系。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 778. 水位上升的泳池中游泳 | 困难 | 同样最小化路径上的最大瓶颈,原题看格子高度,本题看相邻格子的高度差。 |
| 1584. 连接所有点的最小费用 | 中等 | 按边权递增做并查集合并时,起终点首次连通的权值给出本题瓶颈;原题继续连接全部点求总成本。 |
| 743. 网络延迟时间 | 中等 | 用优先队列取当前最优距离并松弛邻边;本题路径代价改为沿途最大边权,该题传播源点到各点的加法距离。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!