title: Path With Minimum Effort slug: path-with-minimum-effort difficulty: medium leetcode_id: 1631 leetcode_url: https://leetcode.com/problems/path-with-minimum-effort/ categories: - graphs - arrays - binary-search - heap patterns: - binary-search - bfs - heap - matrix-traversal description: | You are a hiker preparing for an upcoming hike. 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*. constraints: | - `rows == heights.length` - `columns == heights[i].length` - `1 <= rows, columns <= 100` - `1 <= heights[i][j] <= 10^6` examples: - input: "heights = [[1,2,2],[3,8,2],[5,3,5]]" output: "2" explanation: "The route of [1,3,5,3,5] has a maximum absolute difference of 2 in consecutive cells. This is better than the route of [1,2,2,2,5], where the maximum absolute difference is 3." - input: "heights = [[1,2,3],[3,8,4],[5,3,5]]" output: "1" explanation: "The route of [1,2,3,4,5] has a maximum absolute difference of 1 in consecutive cells, which is better than route [1,3,5,3,5]." - 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." explanation: intuition: | Imagine hiking through a mountainous terrain represented as a grid. At each step, you feel the "effort" of climbing up or down — the bigger the height difference, the harder it is. Your goal is to find a path from the top-left corner to the bottom-right corner that minimises the **worst single step** along the way. The key insight is that we're not minimising the *total* effort (sum of all differences), but rather the *maximum* effort in any single step. Think of it like this: if your worst step has a height difference of 5, that defines your path's difficulty — even if every other step is easy. This "minimax" objective suggests a different approach from typical shortest path problems. We have two elegant strategies: 1. **Binary search on the answer**: What if we could ask, "Can I reach the destination if my maximum allowed effort is `k`?" If yes, try a smaller `k`. If no, we need a larger `k`. This transforms the optimisation problem into a series of yes/no reachability checks. 2. **Modified Dijkstra's algorithm**: Instead of minimising total distance, we track the minimum "maximum effort so far" to reach each cell. Using a min-heap prioritised by effort, we always expand the most promising path first. approach: | We present two approaches: **Binary Search + BFS** and **Dijkstra's Algorithm**. **Approach A: Binary Search + BFS** **Step 1: Define the search space** - The minimum possible effort is `0` (all cells have the same height) - The maximum possible effort is `10^6 - 1` (maximum height difference based on constraints) - We binary search on this range to find the minimum effort that allows us to reach the destination   **Step 2: Implement the feasibility check** - Given a maximum allowed effort `k`, use BFS to check if we can reach `(rows-1, cols-1)` from `(0, 0)` - Only traverse edges where the height difference is `<= k` - If we reach the destination, the answer is `k` or lower; otherwise, we need a larger effort   **Step 3: Binary search for the answer** - If `can_reach(mid)` is true, search the lower half (`right = mid`) - If false, search the upper half (`left = mid + 1`) - Return `left` when the search converges   **Approach B: Dijkstra's Algorithm** **Step 1: Initialise data structures** - `effort`: 2D array tracking the minimum effort to reach each cell (initialised to infinity) - `effort[0][0] = 0`: Starting cell requires zero effort - Min-heap: `[(0, 0, 0)]` representing `(current_max_effort, row, col)`   **Step 2: Process cells greedily** - Pop the cell with the smallest effort from the heap - If we've reached `(rows-1, cols-1)`, return the effort immediately - For each neighbour, calculate the new effort: `max(current_effort, |height_diff|)` - If this is better than the previously recorded effort for that neighbour, update and add to heap   **Step 3: Return the result** - The first time we pop the destination cell, we have found the minimum effort   Dijkstra's works here because we're using a min-heap: we always process the path with the smallest maximum effort first, guaranteeing optimality. common_pitfalls: - title: Using Standard BFS/DFS Alone description: | Standard BFS finds the shortest path by number of edges, and DFS explores deeply first. Neither directly minimises the maximum edge weight along a path. BFS/DFS can be used as a subroutine (to check reachability given a constraint), but you need an outer structure (binary search or Dijkstra's priority queue) to find the optimal answer. wrong_approach: "Plain BFS/DFS to find path with minimum effort" correct_approach: "Binary Search + BFS or Dijkstra's algorithm" - title: Minimising Total Effort Instead of Maximum description: | This problem asks for the **minimum of the maximum** effort along any path — not the sum of all efforts. This is a minimax problem. For example, a path with steps [3, 1, 1, 1] has effort 3 (the max), while [2, 2, 2, 2] has effort 2 — even though the latter has a higher sum. wrong_approach: "Summing height differences along the path" correct_approach: "Tracking maximum height difference along each path" - title: Not Using a Min-Heap in Dijkstra's description: | Without a min-heap, you lose the greedy property that makes Dijkstra's work. You might process a cell via a suboptimal path first, then need to reprocess it later. The min-heap ensures we always expand the path with the smallest maximum effort, so the first time we reach the destination is guaranteed to be optimal. wrong_approach: "Using a regular queue or processing in arbitrary order" correct_approach: "Use heapq with (effort, row, col) tuples" - title: Forgetting to Check All Four Directions description: | Unlike some grid problems, this one allows movement in all four directions (up, down, left, right). Missing a direction means potentially missing the optimal path. Always iterate over all four directions: `[(0,1), (0,-1), (1,0), (-1,0)]`. wrong_approach: "Only moving right and down" correct_approach: "Exploring all four orthogonal directions" key_takeaways: - "**Minimax problems**: When minimising the maximum cost, binary search on the answer or modified Dijkstra's are powerful techniques" - "**Binary search on answer space**: Transform 'find minimum X' into 'is X achievable?' and binary search" - "**Modified Dijkstra's**: Works for any problem where you need the 'best' path under a monotonic metric — not just shortest distance" - "**Grid as graph**: Cells are nodes, adjacent cells are edges with weight equal to height difference" time_complexity: "O(m × n × log(maxHeight)) for binary search approach, O(m × n × log(m × n)) for Dijkstra's. Both are efficient for the given constraints." space_complexity: "O(m × n). We need space for the visited/effort array and the BFS queue or Dijkstra's heap." solutions: - approach_name: Dijkstra's Algorithm is_optimal: true code: | import heapq def minimum_effort_path(heights: list[list[int]]) -> int: rows, cols = len(heights), len(heights[0]) # effort[r][c] = minimum effort to reach cell (r, c) effort = [[float('inf')] * cols for _ in range(rows)] effort[0][0] = 0 # Min-heap: (current_max_effort, row, col) heap = [(0, 0, 0)] directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] while heap: curr_effort, r, c = heapq.heappop(heap) # Found the destination - return immediately if r == rows - 1 and c == cols - 1: return curr_effort # Skip if we've found a better path to this cell if curr_effort > effort[r][c]: continue # Explore all four neighbours for dr, dc in directions: nr, nc = r + dr, c + dc if 0 <= nr < rows and 0 <= nc < cols: # Effort to reach neighbour = max of current effort and this edge height_diff = abs(heights[nr][nc] - heights[r][c]) new_effort = max(curr_effort, height_diff) # Only update if we found a better path if new_effort < effort[nr][nc]: effort[nr][nc] = new_effort heapq.heappush(heap, (new_effort, nr, nc)) return effort[rows - 1][cols - 1] explanation: | **Time Complexity:** O(m × n × log(m × n)) — Each cell can be added to the heap multiple times, but we process at most O(m × n) pops. Each heap operation is O(log(m × n)). **Space Complexity:** O(m × n) — For the effort array and heap. We use a modified Dijkstra's algorithm where instead of summing edge weights, we track the maximum edge weight along the path. The min-heap ensures we always process the most promising path first. - approach_name: Binary Search + BFS is_optimal: true code: | from collections import deque def minimum_effort_path(heights: list[list[int]]) -> int: rows, cols = len(heights), len(heights[0]) directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] def can_reach(max_effort: int) -> bool: """Check if we can reach destination with at most max_effort.""" if max_effort < 0: return False visited = [[False] * cols for _ in range(rows)] queue = deque([(0, 0)]) visited[0][0] = True while queue: r, c = queue.popleft() # Reached the destination if r == rows - 1 and c == cols - 1: return True for dr, dc in directions: nr, nc = r + dr, c + dc if 0 <= nr < rows and 0 <= nc < cols and not visited[nr][nc]: # Only traverse if height diff is within allowed effort if abs(heights[nr][nc] - heights[r][c]) <= max_effort: visited[nr][nc] = True queue.append((nr, nc)) return False # Binary search on the answer left, right = 0, 10**6 - 1 while left < right: mid = (left + right) // 2 if can_reach(mid): right = mid # Try smaller effort else: left = mid + 1 # Need more effort return left explanation: | **Time Complexity:** O(m × n × log(maxHeight)) — Binary search over the effort range (log(10^6) iterations), each requiring O(m × n) BFS. **Space Complexity:** O(m × n) — For the visited array and BFS queue. We binary search on the answer: for each candidate effort `k`, BFS checks if we can reach the destination using only edges with height difference ≤ `k`. If yes, we try smaller; if no, we try larger.