Skip to content
JZLeetCode
Go back

LeetCode 1631 Path With Minimum Effort

Table of contents

Open Table of contents

Description

Question Links: LeetCode 1631

You are a hiker preparing for an upcoming hike. You are given heights, a 2D array of size rows x cols, where heights[row][col] represents the height of cell (row, col). You are situated in the top-left cell (0, 0), and you hope to travel to the bottom-right cell (rows-1, cols-1). You can move up, down, left, or right, and you wish to find a route that requires the minimum effort.

A route’s effort is the maximum absolute difference in heights between two consecutive cells of the route.

Return the minimum effort required to travel from the top-left cell to the bottom-right cell.

Example 1:

Input: heights = [[1,2,2],[3,8,2],[5,3,5]]
Output: 2
Explanation: The route [1,3,5,3,5] has a maximum absolute difference of 2.

Example 2:

Input: heights = [[1,2,3],[3,8,4],[5,3,5]]
Output: 1
Explanation: The route [1,2,3,4,5] has a maximum absolute difference of 1.

Example 3:

Input: 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]]
Output: 0
Explanation: This route does not require any effort.

Constraints:

Idea1: Dijkstra

This is a shortest-path problem where the “distance” is defined as the maximum edge weight along a path — a minimax path problem. We can solve it with a modified Dijkstra: use a min-heap of (effort, row, col), and for each neighbor compute new_effort = max(current_effort, |heights[r][c] - heights[nr][nc]|). Update dist[nr][nc] if we find a smaller effort.

heights:         dist after Dijkstra:
1  2  2          0  1  1
3  8  2          2  6  1
5  3  5          2  2  2   <- answer is dist[2][2] = 2

Path: (0,0)->(0,1)->(0,2)->  edges: |1-2|=1, |2-2|=0
      (1,2)->                 edge:  |2-2|=0
      (2,2)                   edge:  |2-5|=3? No, better path:
                              (0,0)->(1,0)->(2,0)->(2,1)->(2,2)
                              edges: 2, 2, 2, 2 -> max = 2

Complexity: Time O(mnlog(mn))O(m \cdot n \cdot \log(m \cdot n)) — each cell pushed to heap at most once with improvement. Space O(mn)O(m \cdot n) — dist array and heap.

Idea2: Binary Search + BFS

Binary search on the answer in range [0,106][0, 10^6]. For a given threshold mid, use BFS to check if there exists a path from (0,0) to (rows-1, cols-1) where every edge has |diff| <= mid. If reachable, search lower; otherwise search higher.

heights = [[1,2,2],[3,8,2],[5,3,5]]

mid = 1: BFS from (0,0) can reach (0,1),(0,2),(1,2) but not (2,2). Fail.
mid = 2: BFS from (0,0) can reach (1,0),(2,0),(2,1),(2,2). Success!
Answer = 2.

Complexity: Time O(mnlog(max_height))O(m \cdot n \cdot \log(\text{max\_height}))log(106)20\log(10^6) \approx 20 BFS passes. Space O(mn)O(m \cdot n) — visited array and queue.

Java

// Dijkstra. O(m*n*log(m*n)) time, O(m*n) space.
public static int minimumEffortPathDijkstra(int[][] heights) {
    int rows = heights.length, cols = heights[0].length;
    int[][] dist = new int[rows][cols];
    for (int[] row : dist) Arrays.fill(row, Integer.MAX_VALUE);
    dist[0][0] = 0;
    PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[0]));
    pq.offer(new int[]{0, 0, 0});
    int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
    while (!pq.isEmpty()) {
        int[] curr = pq.poll();
        int effort = curr[0], row = curr[1], col = curr[2];
        if (row == rows - 1 && col == cols - 1) return effort;
        if (effort > dist[row][col]) continue;
        for (int[] dir : directions) { // O(4) neighbors
            int nr = row + dir[0], nc = col + dir[1];
            if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
            int newEffort = Math.max(effort, Math.abs(heights[nr][nc] - heights[row][col]));
            if (newEffort < dist[nr][nc]) { // relaxation
                dist[nr][nc] = newEffort;
                pq.offer(new int[]{newEffort, nr, nc});
            }
        }
    }
    return dist[rows - 1][cols - 1];
}
// Binary search + BFS. O(m*n*log(max_height)) time, O(m*n) space.
public static int minimumEffortPathBinarySearch(int[][] heights) {
    int left = 0, right = 1_000_000;
    while (left < right) { // O(log(10^6)) iterations
        int mid = left + (right - left) / 2;
        if (canReachWithEffort(heights, mid)) right = mid; // O(m*n) BFS
        else left = mid + 1;
    }
    return left;
}

private static boolean canReachWithEffort(int[][] heights, int maxEffort) {
    int rows = heights.length, cols = heights[0].length;
    boolean[][] visited = new boolean[rows][cols];
    Queue<int[]> queue = new ArrayDeque<>();
    queue.offer(new int[]{0, 0});
    visited[0][0] = true;
    int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
    while (!queue.isEmpty()) {
        int[] curr = queue.poll();
        int r = curr[0], c = curr[1];
        if (r == rows - 1 && c == cols - 1) return true;
        for (int[] dir : directions) {
            int nr = r + dir[0], nc = c + dir[1];
            if (nr < 0 || nr >= rows || nc < 0 || nc >= cols || visited[nr][nc]) continue;
            if (Math.abs(heights[nr][nc] - heights[r][c]) <= maxEffort) {
                visited[nr][nc] = true;
                queue.offer(new int[]{nr, nc});
            }
        }
    }
    return false;
}

Python

# Dijkstra. O(m*n*log(m*n)) time, O(m*n) space.
import heapq

class Solution:
    def minimumEffortPath(self, heights: list[list[int]]) -> int:
        rows, cols = len(heights), len(heights[0])
        dist = [[float('inf')] * cols for _ in range(rows)]
        dist[0][0] = 0
        heap = [(0, 0, 0)]  # (effort, row, col)
        while heap:
            effort, r, c = heapq.heappop(heap)
            if r == rows - 1 and c == cols - 1:
                return effort
            if effort > dist[r][c]:
                continue
            for dr, dc in ((0, 1), (0, -1), (1, 0), (-1, 0)):  # O(4) neighbors
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols:
                    new_effort = max(effort, abs(heights[r][c] - heights[nr][nc]))
                    if new_effort < dist[nr][nc]:  # relaxation
                        dist[nr][nc] = new_effort
                        heapq.heappush(heap, (new_effort, nr, nc))
        return 0
# Binary search + BFS. O(m*n*log(max_height)) time, O(m*n) space.
from collections import deque

class Solution2:
    def minimumEffortPath(self, heights: list[list[int]]) -> int:
        rows, cols = len(heights), len(heights[0])

        def can_reach(threshold: int) -> bool:
            visited = [[False] * cols for _ in range(rows)]
            visited[0][0] = True
            queue = deque([(0, 0)])
            while queue:
                r, c = queue.popleft()
                if r == rows - 1 and c == cols - 1:
                    return True
                for dr, dc in ((0, 1), (0, -1), (1, 0), (-1, 0)):
                    nr, nc = r + dr, c + dc
                    if 0 <= nr < rows and 0 <= nc < cols and not visited[nr][nc]:
                        if abs(heights[r][c] - heights[nr][nc]) <= threshold:
                            visited[nr][nc] = True
                            queue.append((nr, nc))
            return False

        lo, hi = 0, 10**6  # O(log(10^6)) iterations
        while lo < hi:
            mid = (lo + hi) // 2
            if can_reach(mid):  # O(m*n) BFS
                hi = mid
            else:
                lo = mid + 1
        return lo

C++

// Dijkstra. O(m*n*log(m*n)) time, O(m*n) space.
int dijkstra(vector<vector<int>>& heights) {
    int rows = heights.size(), cols = heights[0].size();
    vector<vector<int>> dist(rows, vector<int>(cols, INT_MAX));
    dist[0][0] = 0;
    using State = tuple<int, int, int>;
    priority_queue<State, vector<State>, greater<State>> pq;
    pq.push({0, 0, 0});
    int dirs[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    while (!pq.empty()) {
        auto [effort, r, c] = pq.top(); pq.pop();
        if (r == rows - 1 && c == cols - 1) return effort;
        if (effort > dist[r][c]) continue;
        for (auto& d : dirs) { // O(4) neighbors
            int nr = r + d[0], nc = c + d[1];
            if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
            int newEffort = max(effort, abs(heights[nr][nc] - heights[r][c]));
            if (newEffort < dist[nr][nc]) { // relaxation
                dist[nr][nc] = newEffort;
                pq.push({newEffort, nr, nc});
            }
        }
    }
    return dist[rows - 1][cols - 1];
}
// Binary search + BFS. O(m*n*log(max_height)) time, O(m*n) space.
int binarySearch(vector<vector<int>>& heights) {
    int rows = heights.size(), cols = heights[0].size();
    int left = 0, right = 1000000;
    while (left < right) { // O(log(10^6)) iterations
        int mid = left + (right - left) / 2;
        if (canReach(heights, mid)) right = mid; // O(m*n) BFS
        else left = mid + 1;
    }
    return left;
}

bool canReach(vector<vector<int>>& heights, int maxEffort) {
    int rows = heights.size(), cols = heights[0].size();
    int dirs[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    vector<vector<bool>> visited(rows, vector<bool>(cols, false));
    queue<pair<int, int>> q;
    q.push({0, 0}); visited[0][0] = true;
    while (!q.empty()) {
        auto [r, c] = q.front(); q.pop();
        if (r == rows - 1 && c == cols - 1) return true;
        for (auto& d : dirs) {
            int nr = r + d[0], nc = c + d[1];
            if (nr < 0 || nr >= rows || nc < 0 || nc >= cols || visited[nr][nc]) continue;
            if (abs(heights[nr][nc] - heights[r][c]) <= maxEffort) {
                visited[nr][nc] = true;
                q.push({nr, nc});
            }
        }
    }
    return false;
}

Rust

// Dijkstra. O(m*n*log(m*n)) time, O(m*n) space.
use std::cmp::Reverse;
use std::collections::BinaryHeap;

pub fn minimum_effort_path_dijkstra(heights: Vec<Vec<i32>>) -> i32 {
    let (rows, cols) = (heights.len(), heights[0].len());
    let mut dist = vec![vec![i32::MAX; cols]; rows];
    dist[0][0] = 0;
    let mut heap = BinaryHeap::new();
    heap.push(Reverse((0, 0, 0)));
    let directions = [(0, 1), (1, 0), (0, -1), (-1, 0)];
    while let Some(Reverse((effort, r, c))) = heap.pop() {
        if r == rows - 1 && c == cols - 1 { return effort; }
        if effort > dist[r][c] { continue; }
        for &(dr, dc) in &directions { // O(4) neighbors
            let (nr, nc) = (r as i32 + dr, c as i32 + dc);
            if nr >= 0 && nr < rows as i32 && nc >= 0 && nc < cols as i32 {
                let (nr, nc) = (nr as usize, nc as usize);
                let new_effort = effort.max((heights[r][c] - heights[nr][nc]).abs());
                if new_effort < dist[nr][nc] { // relaxation
                    dist[nr][nc] = new_effort;
                    heap.push(Reverse((new_effort, nr, nc)));
                }
            }
        }
    }
    dist[rows - 1][cols - 1]
}
// Binary search + BFS. O(m*n*log(max_height)) time, O(m*n) space.
use std::collections::VecDeque;

pub fn minimum_effort_path_binary_search(heights: Vec<Vec<i32>>) -> i32 {
    let (rows, cols) = (heights.len(), heights[0].len());
    let (mut lo, mut hi) = (0, 1_000_000);
    while lo < hi { // O(log(10^6)) iterations
        let mid = lo + (hi - lo) / 2;
        if can_reach(mid, &heights, rows, cols) { hi = mid; } // O(m*n) BFS
        else { lo = mid + 1; }
    }
    lo
}

fn can_reach(max_effort: i32, heights: &[Vec<i32>], rows: usize, cols: usize) -> bool {
    let mut visited = vec![vec![false; cols]; rows];
    let mut queue = VecDeque::new();
    queue.push_back((0, 0));
    visited[0][0] = true;
    let directions = [(0, 1), (1, 0), (0, -1), (-1, 0)];
    while let Some((r, c)) = queue.pop_front() {
        if r == rows - 1 && c == cols - 1 { return true; }
        for &(dr, dc) in &directions {
            let (nr, nc) = (r as i32 + dr, c as i32 + dc);
            if nr >= 0 && nr < rows as i32 && nc >= 0 && nc < cols as i32 {
                let (nr, nc) = (nr as usize, nc as usize);
                if !visited[nr][nc] && (heights[r][c] - heights[nr][nc]).abs() <= max_effort {
                    visited[nr][nc] = true;
                    queue.push_back((nr, nc));
                }
            }
        }
    }
    false
}
Share this post on:

Previous Post
LeetCode 75 Sort Colors
Next Post
LeetCode 36 Valid Sudoku