questions M-R

This commit is contained in:
2025-05-25 12:43:25 +01:00
parent ad320dc703
commit 0a0feb93b5
62 changed files with 12841 additions and 0 deletions

View File

@@ -0,0 +1,255 @@
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
&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.