1631. Path with Minimum Effort - Explanation

Problem Link

Description

You are given heights, a 2D array of size rows x columns, 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, columns-1) (i.e., 0-indexed). 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,1,1],
    [3,2,4],
    [2,5,4]
]

Output: 2

Explanation: The route of [1,1,2,4,4] has a maximum absolute difference of 2 in consecutive cells.

Example 2:

Input: heights = [
    [1,1,1],
    [1,1,2],
    [6,5,2]
]

Output: 1

Explanation: The route of [1,1,1,1,1,2,2] has a maximum absolute difference of 1 in consecutive cells.

Constraints:

  • row == heights.length
  • col == heights[i].length
  • 1 <= row, col <= 100
  • 1 <= heights[i][j].length <= 1,000,000 .


Topics

Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Dijkstra's Algorithm - Finding shortest paths using a priority queue with modified cost function
  • Binary Search - Searching for the minimum feasible effort value
  • Depth First Search (DFS) - Checking path reachability under a given constraint
  • Union-Find / Disjoint Set Union (DSU) - Connecting components to determine reachability (Kruskal's approach)
  • 2D Grid Traversal - Moving in four directions and handling grid boundaries

1. Dijkstra's Algorithm

Intuition

The effort of a path is defined as the maximum absolute difference between consecutive cells along that path. We want to minimize this maximum difference. Dijkstra's algorithm fits well because we can treat the "effort so far" as the cost and always expand the path with the smallest maximum effort. When we reach the destination, we have found the path with minimum effort.

Algorithm

  1. Initialize a min-heap with (0, 0, 0) representing (effort, row, col) starting at the top-left cell.
  2. Maintain a distance array where dist[r][c] stores the minimum effort to reach cell (r, c). Initialize all values to infinity except dist[0][0] = 0.
  3. Pop the cell with the smallest effort from the heap. If it is the destination, return the effort.
  4. For each of the four neighbors, calculate the new effort as the maximum of the current effort and the absolute height difference.
  5. If this new effort is better than the recorded distance for the neighbor, update it and push the neighbor to the heap.
  6. Continue until the destination is reached.
class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        ROWS, COLS = len(heights), len(heights[0])
        minHeap = [[0, 0, 0]]  # [diff, row, col]
        visit = set()
        directions = [[0, 1], [0, -1], [1, 0], [-1, 0]]

        while minHeap:
            diff, r, c = heapq.heappop(minHeap)

            if (r, c) in visit:
                continue

            visit.add((r, c))

            if (r, c) == (ROWS - 1, COLS - 1):
                return diff

            for dr, dc in directions:
                newR, newC = r + dr, c + dc
                if (
                    newR < 0 or newC < 0 or
                    newR >= ROWS or newC >= COLS or
                    (newR, newC) in visit
                ):
                    continue

                newDiff = max(diff, abs(heights[r][c] - heights[newR][newC]))
                heapq.heappush(minHeap, [newDiff, newR, newC])

        return 0

Time & Space Complexity

  • Time complexity: O(mnlog(mn))O(m * n * \log (m * n))
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns in the given matrix.


2. Binary Search + DFS

Intuition

We can binary search on the answer. For a given effort limit, we check whether it is possible to reach the destination using only edges with absolute differences at most that limit. dfs explores if a valid path exists under the constraint. If a path exists, we try a smaller limit; otherwise, we try a larger one.

Algorithm

  1. Binary search on the effort limit between 0 and 1,000,000 (the maximum possible difference).
  2. For each candidate limit (mid), run a dfs from (0, 0) to check if we can reach (ROWS-1, COLS-1) using only edges with absolute difference at most mid.
  3. In the dfs, mark cells as visited and only move to neighbors where the height difference is within the limit.
  4. If the dfs reaches the destination, the limit is feasible. Record it and search for a smaller limit.
  5. If the dfs fails, search for a larger limit.
  6. Return the smallest feasible limit found.
class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        ROWS, COLS = len(heights), len(heights[0])
        directions = [[0, 1], [0, -1], [1, 0], [-1, 0]]

        def dfs(r, c, limit, visited):
            if r == ROWS - 1 and c == COLS - 1:
                return True

            visited.add((r, c))
            for dr, dc in directions:
                newR, newC = r + dr, c + dc
                if (newR < 0 or newC < 0 or
                    newR >= ROWS or newC >= COLS or
                    (newR, newC) in visited or
                    abs(heights[newR][newC] - heights[r][c]) > limit):
                    continue

                if dfs(newR, newC, limit, visited):
                    return True
            return False

        l, r = 0, 1000000
        res = r

        while l <= r:
            mid = (l + r) // 2
            if dfs(0, 0, mid, set()):
                res = mid
                r = mid - 1
            else:
                l = mid + 1

        return res

Time & Space Complexity

  • Time complexity: O(mnlog(mn))O(m * n * \log (m * n))
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns in the given matrix.


3. Kruskal's Algorithm

Intuition

We can view the grid as a graph where each cell is a node and edges connect adjacent cells with weights equal to the absolute height difference. The problem becomes finding the path from top-left to bottom-right that minimizes the maximum edge weight. Kruskal's algorithm processes edges in sorted order, and we stop as soon as the source and destination become connected. The last edge added determines the minimum effort.

Algorithm

  1. Create a list of all edges between adjacent cells, where each edge stores (weight, cell1, cell2). Map each cell to a unique index.
  2. Sort edges by weight in ascending order.
  3. Initialize a Disjoint Set Union (DSU) data structure.
  4. Process edges one by one. For each edge, union the two cells.
  5. After each union, check if the top-left cell (index 0) and bottom-right cell are connected.
  6. If connected, return the current edge weight as the answer.
  7. If the grid has only one cell, return 0.
class DSU:
    def __init__(self, n):
        self.Parent = list(range(n + 1))
        self.Size = [1] * (n + 1)

    def find(self, node):
        if self.Parent[node] != node:
            self.Parent[node] = self.find(self.Parent[node])
        return self.Parent[node]

    def union(self, u, v):
        pu = self.find(u)
        pv = self.find(v)
        if pu == pv:
            return False
        if self.Size[pu] < self.Size[pv]:
            pu, pv = pv, pu
        self.Size[pu] += self.Size[pv]
        self.Parent[pv] = pu
        return True


class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        ROWS, COLS = len(heights), len(heights[0])
        edges = []
        for r in range(ROWS):
            for c in range(COLS):
                if r + 1 < ROWS:
                    edges.append((abs(heights[r][c] - heights[r + 1][c]), r * COLS + c, (r + 1) * COLS + c))
                if c + 1 < COLS:
                    edges.append((abs(heights[r][c] - heights[r][c + 1]), r * COLS + c, r * COLS + c + 1))

        edges.sort()
        dsu = DSU(ROWS * COLS)
        for weight, u, v in edges:
            dsu.union(u, v)
            if dsu.find(0) == dsu.find(ROWS * COLS - 1):
                return weight
        return 0

Time & Space Complexity

  • Time complexity: O(mnlog(mn))O(m * n * \log (m * n))
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns in the given matrix.


4. Shortest Path Faster Algorithm

Intuition

SPFA can also solve this problem by treating effort as the distance metric. We use a queue to process cells and update their minimum effort when a better path is found. Unlike Dijkstra's, SPFA does not require a priority queue, trading off guaranteed optimal ordering for simplicity and potential efficiency on some inputs.

Algorithm

  1. Initialize a dist array with infinity for all cells except dist[0] = 0 for the starting cell.
  2. Use a queue and add the starting cell. Track which cells are currently in the queue.
  3. Dequeue a cell and mark it as not in the queue.
  4. For each neighbor, compute the new effort as max(current effort, height difference).
  5. If the new effort is less than the neighbor's recorded effort, update it. If the neighbor is not in the queue, add it.
  6. Continue until the queue is empty and return dist for the bottom-right cell.
class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        ROWS, COLS = len(heights), len(heights[0])
        dist = [float('inf')] * (ROWS * COLS)
        dist[0] = 0
        in_queue = [False] * (ROWS * COLS)

        def index(r, c):
            return r * COLS + c

        queue = deque([0])
        in_queue[0] = True
        directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]

        while queue:
            u = queue.popleft()
            in_queue[u] = False
            r, c = divmod(u, COLS)

            for dr, dc in directions:
                newR, newC = r + dr, c + dc
                if 0 <= newR < ROWS and 0 <= newC < COLS:
                    v = index(newR, newC)
                    weight = abs(heights[r][c] - heights[newR][newC])
                    new_dist = max(dist[u], weight)
                    if new_dist < dist[v]:
                        dist[v] = new_dist
                        if not in_queue[v]:
                            queue.append(v)
                            in_queue[v] = True

        return dist[ROWS * COLS - 1]

Time & Space Complexity

  • Time complexity:
    • O(mn)O(m * n) time in average case.
    • O(m2n2)O(m ^ 2 * n ^ 2) time in worst case.
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns in the given matrix.


Common Pitfalls

Summing Differences Instead of Taking Maximum

The effort of a path is defined as the maximum absolute difference along the path, not the sum. Using addition instead of max() when computing the new effort will give incorrect results. Always compute newEffort = max(currentEffort, abs(height[current] - height[neighbor])).

Confusing Single-Cell Grids

When the grid has only one cell, the answer is 0 since we are already at the destination. Some implementations may fail to handle this edge case, especially when the algorithm expects at least one move. Check for ROWS == 1 && COLS == 1 as a base case or ensure your algorithm naturally returns 0.

Incorrect Distance Initialization

In Dijkstra's algorithm, all distances should be initialized to infinity except for the starting cell which should be 0. Some implementations mistakenly initialize all distances to zero or forget to set the starting distance, causing the algorithm to skip valid relaxations or produce incorrect minimum efforts.