Files
codetutor/backend/data/questions/path-with-minimum-effort.yaml

276 lines
12 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
function_signature: "def minimum_effort_path(heights: list[list[int]]) -> int:"
test_cases:
visible:
- input: { heights: [[1, 2, 2], [3, 8, 2], [5, 3, 5]] }
expected: 2
- input: { heights: [[1, 2, 3], [3, 8, 4], [5, 3, 5]] }
expected: 1
- 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]] }
expected: 0
hidden:
- input: { heights: [[1]] }
expected: 0
- input: { heights: [[1, 2]] }
expected: 1
- input: { heights: [[1], [2]] }
expected: 1
- input: { heights: [[1, 10, 6, 7, 9, 10, 4, 9]] }
expected: 9
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
&nbsp;
**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
&nbsp;
**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
&nbsp;
**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)`
&nbsp;
**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
&nbsp;
**Step 3: Return the result**
- The first time we pop the destination cell, we have found the minimum effort
&nbsp;
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.