Path With Minimum Effort made easy: understand LeetCode 1631 with a Dijkstra heap, 4-way moves, and fully commented Python code for students.
Here’s the [Problem Link] to begin with.
1. What does the problem ask?
You get an m × n
matrix heights
, where heights[r][c]
is the altitude of cell (r, c)
.
- You start at the top-left cell
(0,0)
and must reach the bottom-right cell(m-1,n-1)
. - You may step up, down, left, or right (no diagonals).
- Effort of a path = the largest absolute height difference between any two consecutive cells along that path.
Return the smallest possible effort among all valid paths.
2. Tiny examples
heights | Best path | Effort |
---|---|---|
[[1,2,2],[3,8,2],[5,3,5]] | 1→2→2→2→3→5 | 2 (max diff 2) |
[[1,2,3],[3,8,4],[5,3,5]] | 1→2→3→4→5 | 1 |
[[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]] | snake-like path | 0 |
3. Intuition & approach
3.1 Why classic shortest-path ideas fail
If we tried normal BFS (each step cost = 1) we would only count how many steps, ignoring height gaps.
If we tried Dijkstra with “edge weight = absolute height diff”, we would minimise total sum, not the maximum diff.
3.2 The trick: treat effort so far as the key
Think of each move having a cost equal to its height gap, but our goal is to minimise the worst cost seen so far on the path.
Dijkstra’s greedy rule still works if we define the key of each cell as:
effort(cell) = min possible maximum gap to reach this cell
Because the algorithm always pops the currently smallest effort cell, once we pop the bottom-right cell we are guaranteed that no smaller effort exists.
3.3 Algorithm steps
- Distance grid
effort_arr
– keeps best known effort for every cell, initialised to∞
, excepteffort_arr[0][0] = 0
. - Min-heap – store
(current_effort, row, col)
. Start with(0,0,0)
. - Loop until heap is empty
- Pop the cell with the smallest effort so far.
- If it is the goal, return its effort (greedy guarantee).
- For each of the four neighbours inside the grid and not blocked:
step_gap = |height[next] − height[curr]|
new_effort = max(current_effort, step_gap)
- If
new_effort
improves the neighbour’s value ineffort_arr
, update and push it into the heap.
- If we exhaust the heap (the grid is always connected so this won’t happen), return
0
.
Because each cell may improve at most once, the loop is O(m × n log(m × n))
.
4. Code
import sys
import heapq
from typing import List
class Solution:
def minimumEffortPath(self, heights: List[List[int]]) -> int:
rows = len(heights)
cols = len(heights[0])
# 1. distance grid: best effort seen so far for every cell
effort_arr = [[sys.maxsize for _ in range(cols)] for _ in range(rows)]
effort_arr[0][0] = 0
# 2. min-heap with tuples (effort, row, col)
priority_queue = [[0, 0, 0]]
while priority_queue:
eff, i, j = heapq.heappop(priority_queue)
# 3. goal reached – current effort is already minimal
if i == rows - 1 and j == cols - 1:
return eff
# 4. explore four neighbours
for x, y in [[-1, 0], [0, -1], [1, 0], [0, 1]]:
new_i, new_j = i + x, j + y
# skip when out of bounds
if new_i < 0 or new_j < 0 or new_i >= rows or new_j >= cols:
continue
# effort of stepping into neighbour
step_gap = abs(heights[new_i][new_j] - heights[i][j])
new_eff = max(eff, step_gap)
# 5. relax if we found a gentler path
if new_eff < effort_arr[new_i][new_j]:
effort_arr[new_i][new_j] = new_eff
heapq.heappush(priority_queue, [new_eff, new_i, new_j])
5. Line-by-line walkthrough
- Rows & Cols – grid size.
effort_arr
– imagine each cell filled with “∞ degrees of pain”; source cell has 0.- Heap push –
(0,0,0)
means no effort yet at (0,0). - Pop lowest effort – greedily safe because all weights are non-negative.
- Neighbour loop – only 4 directions (problem requirement).
step_gap
– vertical difference between the two cells.new_eff
– worst gap encountered on the candidate path to neighbour.- Relaxation – keep the gentlest path; push neighbour for future processing.
- Early return – the first time we pop the bottom-right, we’re done.
6. Dry run on a 3 × 3 grid
1 2 2
3 8 2
5 3 5
Heap pop (eff,r,c) | Updates & pushes | Key effort_arr cells |
---|---|---|
(0,0,0) | push (1,0,1), (2,1,0) | start 0 |
(1,0,1) | push (1,0,2), (6,1,1) | (0,1)=1 |
(1,0,2) | push (1,1,2) | (0,2)=1 |
(1,1,2) | push (2,2,2), (3,1,1) | (1,2)=1 |
(2,2,2) | goal reached → return 2 | – |
Path taken: (0,0) → (0,1) → (0,2) → (1,2) → (2,2)
with max gap 2.
7. Complexity
Measure | Value | Reason |
---|---|---|
Time | O(m × n log(m × n)) | Each cell enters heap ≤ once; heap ops cost log cells. |
Space | O(m × n) | effort_arr plus heap (worst-case all cells). |
8. Conclusion
- Treat effort as a distance metric whose cost of an edge is its height gap and whose path cost is the maximum edge cost so far.
- Running Dijkstra on that metric directly solves Path With Minimum Effort.
- Early-exit once the destination cell leaves the heap – the answer is already optimal.
Remember: when you see “minimise the maximum along the path”, think of Dijkstra with a custom aggregation like max()
instead of +
.
For any changes to the article, kindly email at code@codeanddebug.in or contact us at +91-9712928220.