LeetCode #1631 Medium

Path With Minimum Effort

Path With Minimum Effort is LeetCode 1631 (Medium). You are given a grid heights with rows × columns cells, where heights[r][c] is the height of that cell. You start at the top-left cell and want to reach the bottom-right cell.

  • From any cell you may move up, down, left or right to a neighbouring cell.
  • A move costs the absolute difference between the two heights.
  • The effort of a route is the largest single move cost along it, not the sum.

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

Constraints
  • rows == heights.length
  • columns == heights[i].length
  • 1 <= rows, columns <= 100
  • 1 <= heights[i][j] <= 10⁶
graphsbinary-searchheapbfs
Open on LeetCode ↗
02

Intuition

Effort is the steepest step you are forced to take, so a long, gentle detour beats a short route with one cliff in it.

That is a shortest-path problem where cost combines differently. In plain Dijkstra, extending a path by an edge adds its weight; here a step of cost s turns the effort into max(effort, s). Dijkstra only needs one property to stay correct: a path never gets cheaper as it grows. A running maximum has that property just like a sum of non-negative weights, so the first time the bottom-right cell leaves the heap, no waiting route can beat it.

How to spot this pattern

Minimise the maximum along a path, as in the path with minimum effort LeetCode problem, is a bottleneck path problem. When the path can move in any direction, reach for a modified Dijkstra (push max(effort, step) instead of effort + step), or for binary search on the answer with a reachability check. Union-find over the edges sorted by cost also works: the answer is the edge that first joins the two corners. The same shape appears in Swim in Rising Water, where the cost is the highest cell rather than the steepest step.

03

Approach

Try it first

Before reading on, decide what number the heap stores for each cell and what replaces the + in Dijkstra's relaxation. Aim for O(m·n·log(m·n)) time.

1

Store the best effort per cell

dist[r][c] = the smallest effort of any route found so far from the start to (r, c). Set every cell to infinity except the start, which is 0 because no step has been taken. Push (0, 0, 0) onto a min-heap.

2

Pop the smallest effort

Pop (effort, r, c). If it is the bottom-right cell, return effort: it is the smallest effort any route can reach it with. If effort > dist[r][c], this is an old entry left behind after a cheaper route was found; skip it.

3

Relax with max instead of plus

For each neighbour, step = |heights[nr][nc] - heights[r][c]| and new_effort = max(effort, step). If new_effort < dist[nr][nc], record it and push the neighbour. A neighbour can be pushed several times as better routes appear; the stale-entry check keeps the extra copies cheap.

04

Path With Minimum Effort solution in Python | C++ | Java

▶1import heapq
▶2 
▶3class Solution:
▶4 def minimumEffortPath(self, heights: List[List[int]]) -> int:
▶5 m, n = len(heights), len(heights[0])
▶6 dist = [[float("inf")] * n for _ in range(m)]
▶7 dist[0][0] = 0
▶8 heap = [(0, 0, 0)]
▶9 while heap:
▶10 effort, r, c = heapq.heappop(heap)
▶11 if r == m - 1 and c == n - 1:
▶12 return effort
▶13 if effort > dist[r][c]:
▶14 continue
▶15 for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
▶16 if 0 <= nr < m and 0 <= nc < n:
▶17 step = abs(heights[nr][nc] - heights[r][c])
▶18 new_effort = max(effort, step)
▶19 if new_effort < dist[nr][nc]:
▶20 dist[nr][nc] = new_effort
▶21 heapq.heappush(heap, (new_effort, nr, nc))
▶22 return 0
heightsdist121033∞24∞6∞min-heapstale copy00,0start: effort 0 at (0,0)
dist[0][0]0no step taken yet
others∞not reached
heap(0, 0,0)
Effort is the largest single step on a path, not the total climbed. The numbers in the gaps are the step costs between neighbours. dist holds the smallest effort found so far to reach each cell. The start needs no step, so it is 0 and goes on the heap; every other cell is ∞.
heightsdist121033∞24∞6∞min-heapstale copyemptypop (0,0) at effort 0: final
popped(0, 0,0)smallest on the heap
dist[0][0]0final now
heap size0
Pop (0,0) with effort 0. It is the smallest effort on the heap, and a path never gets cheaper by growing, so no other route can reach (0,0) with less: its effort is final. Now each neighbour is checked in turn.
heightsdist121033∞2416∞min-heapstale copy11,0(1,0): max(0, 1) = 1 < ∞ → push
step1|2 − 1|
new effort1max(0, 1)
dist[1][0]1was ∞
A route's effort is its worst step, so going on to a neighbour costs the larger of the current effort and the step. Through (0,0), (1,0) can be reached with effort 1, its first route. Record it and push the neighbour.
heightsdist12103322416∞min-heapstale copy11,020,1(0,1): max(0, 2) = 2 < ∞ → push
step2|3 − 1|
new effort2max(0, 2)
dist[0][1]2was ∞
Through (0,0), (0,1) can be reached with effort 2, its first route. Record it and push the neighbour.
heightsdist12103322416∞min-heapstale copy20,1pop (1,0) at effort 1: final
popped(1, 1,0)smallest on the heap
dist[1][0]1final now
heap size1
Pop (1,0) with effort 1. It is the smallest effort on the heap, and a path never gets cheaper by growing, so no other route can reach (1,0) with less: its effort is final. Now each neighbour is checked in turn.
heightsdist12103322416∞min-heapstale copy20,1(0,0): max(1, 1) = 1 ≥ 0 → skip
step1|1 − 2|
new effort1max(1, 1)
dist[0][0]0already final
(0,0) is already final with effort 0. Going back through (1,0) would cost 1, which is not smaller, so nothing is pushed.
heightsdist121033224164min-heapstale copy20,141,1(1,1): max(1, 4) = 4 < ∞ → push
step4|6 − 2|
new effort4max(1, 4)
dist[1][1]4was ∞
Through (1,0), (1,1) can be reached with effort 4, its first route. Record it and push the neighbour.
heightsdist121033224164min-heapstale copy41,1pop (0,1) at effort 2: final
popped(2, 0,1)smallest on the heap
dist[0][1]2final now
heap size1
Pop (0,1) with effort 2. It is the smallest effort on the heap, and a path never gets cheaper by growing, so no other route can reach (0,1) with less: its effort is final. Now each neighbour is checked in turn.
heightsdist121033224163min-heapstale copy31,141,1(1,1): max(2, 3) = 3 < 4 → push
step3|6 − 3|
new effort3max(2, 3)
dist[1][1]3was 4
Through (0,1), (1,1) can be reached with effort 3, better than the 4 found before. Record it and push the neighbour. The old heap copy stays behind and will be skipped as stale.
heightsdist121033224163min-heapstale copy31,141,1(0,0): max(2, 2) = 2 ≥ 0 → skip
step2|1 − 3|
new effort2max(2, 2)
dist[0][0]0already final
(0,0) is already final with effort 0. Going back through (0,1) would cost 2, which is not smaller, so nothing is pushed.
heightsdist121033224163min-heapstale copy41,1bottom-right popped: return 3
popped(3, 1,1)the destination
answer3largest step on the green path
The destination comes off the heap with effort 3, so that is the answer. The heap always hands out the smallest effort first, and no path can get cheaper by growing, so nothing still on the heap could reach here with less. Following each cell back to the neighbour that improved it gives the green path; its worst step, in bold, is the 3.
05

Binary search on the effort with BFS

Binary search over the effort limit from 0 to 10⁶. For each guess, BFS from the top-left using only steps whose height difference is within the limit; if the bottom-right is reached, try a smaller limit, otherwise a larger one.

▶1from collections import deque
▶2 
▶3 
▶4class Solution:
▶5 def minimumEffortPath(self, heights: List[List[int]]) -> int:
▶6 m, n = len(heights), len(heights[0])
▶7 
▶8 def reachable(limit):
▶9 seen = [[False] * n for _ in range(m)]
▶10 seen[0][0] = True
▶11 queue = deque([(0, 0)])
▶12 while queue:
▶13 r, c = queue.popleft()
▶14 if r == m - 1 and c == n - 1:
▶15 return True
▶16 for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
▶17 if (
▶18 0 <= nr < m
▶19 and 0 <= nc < n
▶20 and not seen[nr][nc]
▶21 and abs(heights[nr][nc] - heights[r][c]) <= limit
▶22 ):
▶23 seen[nr][nc] = True
▶24 queue.append((nr, nc))
▶25 return False
▶26 
▶27 lo, hi = 0, 10**6
▶28 while lo < hi:
▶29 mid = (lo + hi) // 2
▶30 if reachable(mid):
▶31 hi = mid
▶32 else:
▶33 lo = mid + 1
▶34 return lo
06

Common pitfalls

Adding step costs instead of taking the maximum

✗ Wrong
new_effort = effort + abs(heights[nr][nc] - heights[r][c])
✓ Right
new_effort = max(effort, abs(heights[nr][nc] - heights[r][c]))

That is ordinary Dijkstra and returns the least total climb. On the first sample it returns 4 (the route through 1, 2, 2, 2, 5 climbs 1+0+0+3) instead of 2, because it prefers a short route with one steep step over a longer, flatter one.

Filling a DP table that only moves right and down

✗ Wrong
f[r][c] = min(max(f[r-1][c], up_step), max(f[r][c-1], left_step))
✓ Right
# no fixed fill order exists: let a min-heap pick the next cell (Dijkstra)

On [[1,2,3],[8,7,5],[6,4,1]] the best route snakes right, then back left along the middle row, then right again, for effort 3. A right/down DP never considers a left move and returns 4.

Returning when the destination is pushed, not popped

✗ Wrong
if new_effort < dist[nr][nc]:
    if (nr, nc) == (m - 1, n - 1):
        return new_effort
✓ Right
effort, r, c = heapq.heappop(heap)
if r == m - 1 and c == n - 1:
    return effort

The first route to reach a cell is not necessarily the best one. In the 2×2 demo the corner is first pushed with effort 4, and only later lowered to 3. A cell's effort is final only when it comes off the heap.

07

Edge cases

1 × 1 grid

The start is the destination, so the first pop returns 0. The destination check comes before the neighbour loop, so no special case is needed.

08

Complexity

Time
O(m·n·log(m·n))
Space
O(m·n)
Each of the m·n cells has at most 4 edges, and each edge can push one heap entry, so the heap holds O(m·n) entries and each push or pop costs O(log(m·n)). The binary-search version is O(m·n·log H), where H is the largest height.
09

Path With Minimum Effort vs similar grid path problems

All three go from the top-left to the bottom-right of a grid. What a path costs decides the tool.

ProblemCost of a pathMovesStandard method
Minimum Path Sum (64)Sum of the cellsRight and down onlyDP in row order
Path With Minimum Effort (1631)Largest height difference between stepsAll 4 directionsDijkstra with max, or binary search + BFS
Swim in Rising Water (778)Highest cell on the pathAll 4 directionsDijkstra with max, or binary search + BFS
10

Path With Minimum Effort FAQ

What does a path with minimum effort Python solution look like?

Use heapq with tuples (effort, r, c), so the heap orders by effort first. Keep a dist grid of float("inf"), pop until the bottom-right cell appears, skip entries whose effort exceeds dist, and push neighbours with max(effort, step) when that beats their dist.