Bird
Raised Fist0
Interview Prepgraphs-dfs-bfsmediumAmazonGoogle

Path With Minimum Effort

Choose your preparation mode4 modes available
🎯
Path With Minimum Effort
mediumGRAPHAmazonGoogle

Imagine you are hiking across a mountainous terrain represented as a grid, and you want to find a path from start to finish that minimizes the steepest climb between adjacent steps.

💡 This problem is about finding a path in a grid that minimizes the maximum effort (height difference) between consecutive steps. Beginners often struggle because it’s not about minimizing total cost but minimizing the maximum edge cost along the path, which requires a different approach than classic shortest path problems.
📋
Problem Statement

Given a 2D grid heights where heights[i][j] represents the height of cell (i, j), find a path from the top-left corner (0, 0) to the bottom-right corner (n-1, m-1) such that the maximum absolute difference in heights between adjacent cells on the path is minimized. Return this minimum possible maximum difference.

1 ≤ n, m ≤ 1000 ≤ heights[i][j] ≤ 10^6
💡
Example
Input"heights = [[1,2,2],[3,8,2],[5,3,5]]"
Output2

One path is (0,0)->(0,1)->(0,2)->(1,2)->(2,2). The maximum height difference along this path is 2, which is minimal.

Input"heights = [[1,2,3],[3,8,4],[5,3,5]]"
Output1

The path (0,0)->(1,0)->(2,0)->(2,1)->(2,2) has maximum height difference 1.

  • Single cell grid → output 0
  • Grid with all equal heights → output 0
  • Grid where only one path exists → output is max difference on that path
  • Grid with large height differences but a path with small max difference exists → output minimal max difference
⚠️
Common Mistakes
Not pruning DFS paths exceeding current best effort

Excessive runtime and possible TLE

✅ Add pruning condition to skip paths with effort > current minimum

Using sum of efforts instead of max effort in Dijkstra

Incorrect answer because problem minimizes max edge, not sum

✅ Update effort as max(current effort, edge difference) instead of sum

Not handling visited cells correctly in BFS/DFS feasibility check

Infinite loops or revisiting cells causing inefficiency

✅ Maintain visited array to mark cells already processed

Incorrect binary search bounds initialization

Binary search may miss correct answer or run inefficiently

✅ Calculate max possible edge difference to set high bound

🧠
Brute Force (DFS with Backtracking)
💡 This approach tries all possible paths from start to end, tracking the maximum effort along each path. It’s very slow but helps understand the problem’s nature and why optimization is needed.

Intuition

Explore every path recursively, keeping track of the maximum height difference encountered so far. Return the minimum among all maximum efforts of all paths.

Algorithm

  1. Start DFS from (0,0) with initial max effort 0.
  2. At each step, move to adjacent cells not visited yet.
  3. Update max effort with the absolute height difference between current and next cell.
  4. When reaching the bottom-right cell, record the max effort and backtrack.
  5. Return the minimum max effort found among all paths.
💡 The challenge is tracking the maximum edge cost along each path and exploring all paths, which leads to exponential time.
</>
Code
from typing import List

class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        rows, cols = len(heights), len(heights[0])
        self.min_effort = float('inf')
        visited = [[False]*cols for _ in range(rows)]

        def dfs(r, c, max_effort):
            if r == rows - 1 and c == cols - 1:
                self.min_effort = min(self.min_effort, max_effort)
                return
            visited[r][c] = True
            directions = [(1,0), (-1,0), (0,1), (0,-1)]
            for dr, dc in directions:
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols and not visited[nr][nc]:
                    effort = abs(heights[r][c] - heights[nr][nc])
                    if effort <= self.min_effort:
                        dfs(nr, nc, max(max_effort, effort))
            visited[r][c] = False

        dfs(0, 0, 0)
        return self.min_effort

# Example usage:
# sol = Solution()
# print(sol.minimumEffortPath([[1,2,2],[3,8,2],[5,3,5]]))  # Output: 2
Line Notes
self.min_effort = float('inf')Initialize to track the minimum maximum effort found so far; ensures any found effort will be smaller initially
visited = [[False]*cols for _ in range(rows)]Keep track of visited cells to avoid cycles and infinite recursion
if r == rows - 1 and c == cols - 1:Base case: reached destination, update min effort with current path's max effort
if effort <= self.min_effort:Prune paths that already exceed current best effort to reduce unnecessary exploration
visited[r][c] = TrueMark current cell as visited before exploring neighbors to prevent revisiting
visited[r][c] = FalseBacktrack by unmarking the cell to allow other paths to use it
import java.util.*;

public class Solution {
    private int minEffort = Integer.MAX_VALUE;
    private int rows, cols;
    private int[][] heights;
    private boolean[][] visited;
    private int[][] directions = {{1,0},{-1,0},{0,1},{0,-1}};

    public int minimumEffortPath(int[][] heights) {
        this.heights = heights;
        rows = heights.length;
        cols = heights[0].length;
        visited = new boolean[rows][cols];
        dfs(0, 0, 0);
        return minEffort;
    }

    private void dfs(int r, int c, int maxEffort) {
        if (r == rows - 1 && c == cols - 1) {
            minEffort = Math.min(minEffort, maxEffort);
            return;
        }
        visited[r][c] = true;
        for (int[] dir : directions) {
            int nr = r + dir[0], nc = c + dir[1];
            if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && !visited[nr][nc]) {
                int effort = Math.abs(heights[r][c] - heights[nr][nc]);
                if (effort <= minEffort) {
                    dfs(nr, nc, Math.max(maxEffort, effort));
                }
            }
        }
        visited[r][c] = false;
    }

    // Example main
    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][] heights = {{1,2,2},{3,8,2},{5,3,5}};
        System.out.println(sol.minimumEffortPath(heights)); // Output: 2
    }
}
Line Notes
private int minEffort = Integer.MAX_VALUE;Track minimum maximum effort found so far to prune paths
visited = new boolean[rows][cols];Boolean matrix to mark visited cells and avoid cycles
if (r == rows - 1 && c == cols - 1)Check if destination reached to update minimum effort
if (effort <= minEffort)Prune exploration of paths that cannot improve current best effort
visited[r][c] = true;Mark current cell as visited before recursive calls
visited[r][c] = false;Backtrack to allow other paths to use this cell
#include <bits/stdc++.h>
using namespace std;

class Solution {
    int minEffort = INT_MAX;
    int rows, cols;
    vector<vector<int>> heights;
    vector<vector<bool>> visited;
    vector<pair<int,int>> directions = {{1,0},{-1,0},{0,1},{0,-1}};

    void dfs(int r, int c, int maxEffort) {
        if (r == rows - 1 && c == cols - 1) {
            minEffort = min(minEffort, maxEffort);
            return;
        }
        visited[r][c] = true;
        for (auto& dir : directions) {
            int nr = r + dir.first, nc = c + dir.second;
            if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && !visited[nr][nc]) {
                int effort = abs(heights[r][c] - heights[nr][nc]);
                if (effort <= minEffort) {
                    dfs(nr, nc, max(maxEffort, effort));
                }
            }
        }
        visited[r][c] = false;
    }

public:
    int minimumEffortPath(vector<vector<int>>& heights) {
        this->heights = heights;
        rows = heights.size();
        cols = heights[0].size();
        visited.assign(rows, vector<bool>(cols, false));
        dfs(0, 0, 0);
        return minEffort;
    }
};

// Example usage:
// int main() {
//     Solution sol;
//     vector<vector<int>> heights = {{1,2,2},{3,8,2},{5,3,5}};
//     cout << sol.minimumEffortPath(heights) << endl; // Output: 2
//     return 0;
// }
Line Notes
int minEffort = INT_MAX;Initialize to track the minimal maximum effort found
visited.assign(rows, vector<bool>(cols, false));Initialize visited matrix to avoid revisiting cells
if (r == rows - 1 && c == cols - 1)Base case: reached destination, update minimal effort
if (effort <= minEffort)Prune paths that cannot improve current best effort
visited[r][c] = true;Mark cell as visited before recursion
visited[r][c] = false;Backtrack to allow other paths to use this cell
var minimumEffortPath = function(heights) {
    const rows = heights.length, cols = heights[0].length;
    let minEffort = Infinity;
    const visited = Array.from({length: rows}, () => Array(cols).fill(false));
    const directions = [[1,0],[-1,0],[0,1],[0,-1]];

    function dfs(r, c, maxEffort) {
        if (r === rows - 1 && c === cols - 1) {
            minEffort = Math.min(minEffort, maxEffort);
            return;
        }
        visited[r][c] = true;
        for (const [dr, dc] of directions) {
            const nr = r + dr, nc = c + dc;
            if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && !visited[nr][nc]) {
                const effort = Math.abs(heights[r][c] - heights[nr][nc]);
                if (effort <= minEffort) {
                    dfs(nr, nc, Math.max(maxEffort, effort));
                }
            }
        }
        visited[r][c] = false;
    }

    dfs(0, 0, 0);
    return minEffort;
};

// Example test
// console.log(minimumEffortPath([[1,2,2],[3,8,2],[5,3,5]])); // Output: 2
Line Notes
let minEffort = Infinity;Track the minimal maximum effort found so far to prune paths
const visited = Array.from({length: rows}, () => Array(cols).fill(false));Track visited cells to avoid cycles and infinite recursion
if (r === rows - 1 && c === cols - 1)Base case: reached destination, update minimal effort
if (effort <= minEffort)Prune paths that cannot improve current best effort
visited[r][c] = true;Mark cell as visited before exploring neighbors
visited[r][c] = false;Backtrack to allow other paths to use this cell
⏱
Complexity
TimeO(4^(n*m))
SpaceO(n*m)

Each cell can move to up to 4 neighbors, exploring all paths leads to exponential time in grid size

💡 For a 3x3 grid, this means potentially thousands of paths; for 100x100, it’s astronomically large and impractical.
Interview Verdict: TLE

This approach is too slow for large inputs but is useful to understand the problem and why pruning or better algorithms are needed.

🧠
Binary Search + BFS/DFS
💡 This approach uses binary search on the answer (effort) and checks feasibility with BFS/DFS. It’s faster than brute force and introduces a powerful technique for optimization problems.

Intuition

Guess a maximum effort and check if a path exists with all edges ≤ guessed effort. Narrow down the guess using binary search.

Algorithm

  1. Set low = 0 and high = max height difference in grid.
  2. While low < high:
  3. mid = (low + high) // 2
  4. Use BFS/DFS to check if path exists with max edge effort ≤ mid.
  5. If path exists, high = mid; else low = mid + 1.
  6. Return low as minimum effort.
💡 The key is converting the problem into a yes/no question and using binary search to find the minimal yes answer.
</>
Code
from typing import List
from collections import deque

class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        rows, cols = len(heights), len(heights[0])

        def can_reach(max_effort):
            visited = [[False]*cols for _ in range(rows)]
            queue = deque([(0,0)])
            visited[0][0] = True
            directions = [(1,0), (-1,0), (0,1), (0,-1)]

            while queue:
                r, c = queue.popleft()
                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]:
                        effort = abs(heights[r][c] - heights[nr][nc])
                        if effort <= max_effort:
                            visited[nr][nc] = True
                            queue.append((nr, nc))
            return False

        low, high = 0, max(max(abs(heights[r][c] - heights[r2][c2])
                                for r2, c2 in [(r+1,c),(r-1,c),(r,c+1),(r,c-1)]
                                if 0 <= r2 < rows and 0 <= c2 < cols)
                       for r in range(rows) for c in range(cols))

        while low < high:
            mid = (low + high) // 2
            if can_reach(mid):
                high = mid
            else:
                low = mid + 1
        return low

# Example usage:
# sol = Solution()
# print(sol.minimumEffortPath([[1,2,2],[3,8,2],[5,3,5]]))  # Output: 2
Line Notes
def can_reach(max_effort):Check if a path exists where all edges have effort ≤ max_effort
queue = deque([(0,0)])Initialize BFS queue starting from top-left cell
if effort <= max_effort:Only traverse edges within allowed effort to ensure feasibility
while low < high:Binary search loop to narrow down the minimal feasible effort
visited = [[False]*cols for _ in range(rows)]Track visited cells to avoid revisiting and infinite loops
import java.util.*;

public class Solution {
    private int rows, cols;
    private int[][] heights;
    private int[][] directions = {{1,0},{-1,0},{0,1},{0,-1}};

    public int minimumEffortPath(int[][] heights) {
        this.heights = heights;
        rows = heights.length;
        cols = heights[0].length;

        int low = 0, high = 0;
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                for (int[] dir : directions) {
                    int nr = r + dir[0], nc = c + dir[1];
                    if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
                        high = Math.max(high, Math.abs(heights[r][c] - heights[nr][nc]));
                    }
                }
            }
        }

        while (low < high) {
            int mid = low + (high - low) / 2;
            if (canReach(mid)) {
                high = mid;
            } else {
                low = mid + 1;
            }
        }
        return low;
    }

    private boolean canReach(int maxEffort) {
        boolean[][] visited = new boolean[rows][cols];
        Queue<int[]> queue = new LinkedList<>();
        queue.offer(new int[]{0,0});
        visited[0][0] = true;

        while (!queue.isEmpty()) {
            int[] cell = queue.poll();
            int r = cell[0], c = cell[1];
            if (r == rows - 1 && c == cols - 1) return true;
            for (int[] dir : directions) {
                int nr = r + dir[0], nc = c + dir[1];
                if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && !visited[nr][nc]) {
                    int effort = Math.abs(heights[r][c] - heights[nr][nc]);
                    if (effort <= maxEffort) {
                        visited[nr][nc] = true;
                        queue.offer(new int[]{nr, nc});
                    }
                }
            }
        }
        return false;
    }

    // Example main
    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][] heights = {{1,2,2},{3,8,2},{5,3,5}};
        System.out.println(sol.minimumEffortPath(heights)); // Output: 2
    }
}
Line Notes
int high = 0;Initialize max possible effort to find upper bound for binary search
while (low < high)Binary search loop to find minimal feasible effort
boolean[][] visited = new boolean[rows][cols];Track visited cells during BFS to avoid cycles
if (effort <= maxEffort)Only traverse edges within allowed effort to ensure path feasibility
#include <bits/stdc++.h>
using namespace std;

class Solution {
    int rows, cols;
    vector<vector<int>> heights;
    vector<pair<int,int>> directions = {{1,0},{-1,0},{0,1},{0,-1}};

    bool canReach(int maxEffort) {
        vector<vector<bool>> visited(rows, vector<bool>(cols, false));
        queue<pair<int,int>> q;
        q.push({0,0});
        visited[0][0] = true;

        while (!q.empty()) {
            auto [r, c] = q.front(); q.pop();
            if (r == rows - 1 && c == cols - 1) return true;
            for (auto& dir : directions) {
                int nr = r + dir.first, nc = c + dir.second;
                if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && !visited[nr][nc]) {
                    int effort = abs(heights[r][c] - heights[nr][nc]);
                    if (effort <= maxEffort) {
                        visited[nr][nc] = true;
                        q.push({nr, nc});
                    }
                }
            }
        }
        return false;
    }

public:
    int minimumEffortPath(vector<vector<int>>& heights) {
        this->heights = heights;
        rows = heights.size();
        cols = heights[0].size();

        int low = 0, high = 0;
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                for (auto& dir : directions) {
                    int nr = r + dir.first, nc = c + dir.second;
                    if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
                        high = max(high, abs(heights[r][c] - heights[nr][nc]));
                    }
                }
            }
        }

        while (low < high) {
            int mid = low + (high - low) / 2;
            if (canReach(mid)) {
                high = mid;
            } else {
                low = mid + 1;
            }
        }
        return low;
    }
};

// Example usage:
// int main() {
//     Solution sol;
//     vector<vector<int>> heights = {{1,2,2},{3,8,2},{5,3,5}};
//     cout << sol.minimumEffortPath(heights) << endl; // Output: 2
//     return 0;
// }
Line Notes
int high = 0;Find max edge difference to set binary search upper bound
while (low < high)Binary search loop to find minimal feasible effort
vector<vector<bool>> visited(rows, vector<bool>(cols, false));Track visited cells in BFS to avoid revisiting
if (effort <= maxEffort)Only traverse edges within allowed effort to ensure path feasibility
var minimumEffortPath = function(heights) {
    const rows = heights.length, cols = heights[0].length;
    const directions = [[1,0],[-1,0],[0,1],[0,-1]];

    let low = 0, high = 0;
    for (let r = 0; r < rows; r++) {
        for (let c = 0; c < cols; c++) {
            for (const [dr, dc] of directions) {
                const nr = r + dr, nc = c + dc;
                if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
                    high = Math.max(high, Math.abs(heights[r][c] - heights[nr][nc]));
                }
            }
        }
    }

    function canReach(maxEffort) {
        const visited = Array.from({length: rows}, () => Array(cols).fill(false));
        const queue = [[0,0]];
        visited[0][0] = true;

        while (queue.length > 0) {
            const [r, c] = queue.shift();
            if (r === rows - 1 && c === cols - 1) return true;
            for (const [dr, dc] of directions) {
                const nr = r + dr, nc = c + dc;
                if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && !visited[nr][nc]) {
                    const effort = Math.abs(heights[r][c] - heights[nr][nc]);
                    if (effort <= maxEffort) {
                        visited[nr][nc] = true;
                        queue.push([nr, nc]);
                    }
                }
            }
        }
        return false;
    }

    while (low < high) {
        const mid = Math.floor((low + high) / 2);
        if (canReach(mid)) {
            high = mid;
        } else {
            low = mid + 1;
        }
    }
    return low;
};

// Example test
// console.log(minimumEffortPath([[1,2,2],[3,8,2],[5,3,5]])); // Output: 2
Line Notes
let low = 0, high = 0;Initialize binary search bounds for minimal and maximal effort
while (low < high)Binary search loop to find minimal feasible effort
const visited = Array.from({length: rows}, () => Array(cols).fill(false));Track visited cells during BFS to avoid cycles
if (effort <= maxEffort)Only traverse edges within allowed effort to ensure path feasibility
⏱
Complexity
TimeO(n*m*log(maxHeightDifference))
SpaceO(n*m)

Binary search runs in log(maxHeightDifference) steps, each BFS takes O(n*m) time

💡 For a 100x100 grid and max height difference 10^6, this is about 10,000 * 20 = 200,000 operations, which is efficient.
Interview Verdict: Accepted

This approach is efficient and practical, demonstrating a powerful technique of binary searching the answer combined with BFS feasibility checks.

🧠
Dijkstra’s Algorithm Variant
💡 This approach models the problem as a graph and uses a modified Dijkstra’s algorithm to find the path minimizing the maximum edge weight rather than sum of weights.

Intuition

Use a priority queue to always explore the path with the smallest current maximum effort, updating neighbors with the minimal maximum effort found so far.

Algorithm

  1. Initialize a 2D array dist with infinity to track minimal max effort to each cell.
  2. Use a min-heap priority queue starting from (0,0) with effort 0.
  3. Pop the cell with smallest current effort from the queue.
  4. For each neighbor, calculate effort = max(current effort, edge difference).
  5. If effort < dist[neighbor], update dist and push neighbor into queue.
  6. Return dist at bottom-right cell after processing.
💡 The key difference from classic Dijkstra is that edge costs combine by max, not sum, requiring careful updates.
</>
Code
from typing import List
import heapq

class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        rows, cols = len(heights), len(heights[0])
        dist = [[float('inf')] * cols for _ in range(rows)]
        dist[0][0] = 0
        heap = [(0, 0, 0)]  # effort, r, c
        directions = [(1,0), (-1,0), (0,1), (0,-1)]

        while heap:
            effort, r, c = heapq.heappop(heap)
            if r == rows - 1 and c == cols - 1:
                return effort
            if effort > dist[r][c]:
                continue
            for dr, dc in directions:
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols:
                    next_effort = max(effort, abs(heights[r][c] - heights[nr][nc]))
                    if next_effort < dist[nr][nc]:
                        dist[nr][nc] = next_effort
                        heapq.heappush(heap, (next_effort, nr, nc))
        return 0

# Example usage:
# sol = Solution()
# print(sol.minimumEffortPath([[1,2,2],[3,8,2],[5,3,5]]))  # Output: 2
Line Notes
dist = [[float('inf')] * cols for _ in range(rows)]Initialize distances with infinity to find minimal max effort for each cell
heap = [(0, 0, 0)]Start from top-left cell with zero effort in min-heap
if effort > dist[r][c]Skip processing if a better path to this cell was already found
next_effort = max(effort, abs(heights[r][c] - heights[nr][nc]))Update effort as max of current path effort and edge difference to neighbor
import java.util.*;

public class Solution {
    private int rows, cols;
    private int[][] heights;
    private int[][] directions = {{1,0},{-1,0},{0,1},{0,-1}};

    public int minimumEffortPath(int[][] heights) {
        this.heights = heights;
        rows = heights.length;
        cols = heights[0].length;

        int[][] dist = new int[rows][cols];
        for (int[] row : dist) Arrays.fill(row, Integer.MAX_VALUE);
        dist[0][0] = 0;

        PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[0]));
        pq.offer(new int[]{0, 0, 0}); // effort, r, c

        while (!pq.isEmpty()) {
            int[] curr = pq.poll();
            int effort = curr[0], r = curr[1], c = curr[2];
            if (r == rows - 1 && c == cols - 1) return effort;
            if (effort > dist[r][c]) continue;

            for (int[] dir : directions) {
                int nr = r + dir[0], nc = c + dir[1];
                if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
                    int nextEffort = Math.max(effort, Math.abs(heights[r][c] - heights[nr][nc]));
                    if (nextEffort < dist[nr][nc]) {
                        dist[nr][nc] = nextEffort;
                        pq.offer(new int[]{nextEffort, nr, nc});
                    }
                }
            }
        }
        return 0;
    }

    // Example main
    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][] heights = {{1,2,2},{3,8,2},{5,3,5}};
        System.out.println(sol.minimumEffortPath(heights)); // Output: 2
    }
}
Line Notes
int[][] dist = new int[rows][cols];Track minimal max effort to reach each cell
PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[0]));Min-heap to always pick cell with smallest current effort
if (effort > dist[r][c]) continue;Skip processing if a better path was already found
int nextEffort = Math.max(effort, Math.abs(heights[r][c] - heights[nr][nc]));Update effort as max of current path effort and edge difference
#include <bits/stdc++.h>
using namespace std;

class Solution {
    int rows, cols;
    vector<vector<int>> heights;
    vector<pair<int,int>> directions = {{1,0},{-1,0},{0,1},{0,-1}};

public:
    int minimumEffortPath(vector<vector<int>>& heights) {
        this->heights = heights;
        rows = heights.size();
        cols = heights[0].size();

        vector<vector<int>> dist(rows, vector<int>(cols, INT_MAX));
        dist[0][0] = 0;

        using T = tuple<int,int,int>;
        priority_queue<T, vector<T>, greater<T>> pq;
        pq.emplace(0, 0, 0); // effort, r, c

        while (!pq.empty()) {
            auto [effort, r, c] = pq.top(); pq.pop();
            if (r == rows - 1 && c == cols - 1) return effort;
            if (effort > dist[r][c]) continue;

            for (auto& dir : directions) {
                int nr = r + dir.first, nc = c + dir.second;
                if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
                    int nextEffort = max(effort, abs(heights[r][c] - heights[nr][nc]));
                    if (nextEffort < dist[nr][nc]) {
                        dist[nr][nc] = nextEffort;
                        pq.emplace(nextEffort, nr, nc);
                    }
                }
            }
        }
        return 0;
    }
};

// Example usage:
// int main() {
//     Solution sol;
//     vector<vector<int>> heights = {{1,2,2},{3,8,2},{5,3,5}};
//     cout << sol.minimumEffortPath(heights) << endl; // Output: 2
//     return 0;
// }
Line Notes
vector<vector<int>> dist(rows, vector<int>(cols, INT_MAX));Initialize distances with max int to find minimal max effort
priority_queue<T, vector<T>, greater<T>> pq;Min-heap priority queue to pick cell with smallest effort
if (effort > dist[r][c]) continue;Skip outdated entries where better path already found
int nextEffort = max(effort, abs(heights[r][c] - heights[nr][nc]));Update effort as max of current path effort and edge difference
var minimumEffortPath = function(heights) {
    const rows = heights.length, cols = heights[0].length;
    const dist = Array.from({length: rows}, () => Array(cols).fill(Infinity));
    dist[0][0] = 0;
    const directions = [[1,0],[-1,0],[0,1],[0,-1]];

    const pq = new MinPriorityQueue({priority: x => x[0]});
    pq.enqueue([0, 0, 0]); // effort, r, c

    while (!pq.isEmpty()) {
        const [effort, r, c] = pq.dequeue().element;
        if (r === rows - 1 && c === cols - 1) return effort;
        if (effort > dist[r][c]) continue;

        for (const [dr, dc] of directions) {
            const nr = r + dr, nc = c + dc;
            if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
                const nextEffort = Math.max(effort, Math.abs(heights[r][c] - heights[nr][nc]));
                if (nextEffort < dist[nr][nc]) {
                    dist[nr][nc] = nextEffort;
                    pq.enqueue([nextEffort, nr, nc]);
                }
            }
        }
    }
    return 0;
};

// Example test
// console.log(minimumEffortPath([[1,2,2],[3,8,2],[5,3,5]])); // Output: 2

// Note: MinPriorityQueue is from '@datastructures-js/priority-queue' or similar library
Line Notes
const dist = Array.from({length: rows}, () => Array(cols).fill(Infinity));Initialize distances with infinity to track minimal max effort
const pq = new MinPriorityQueue({priority: x => x[0]});Min-heap priority queue to pick cell with smallest effort
if (effort > dist[r][c]) continue;Skip stale queue entries where better path exists
const nextEffort = Math.max(effort, Math.abs(heights[r][c] - heights[nr][nc]));Update effort as max of current path effort and edge difference
⏱
Complexity
TimeO(n*m*log(n*m))
SpaceO(n*m)

Dijkstra visits each cell and edges, priority queue operations cost log(n*m)

💡 For a 100x100 grid, this is about 10,000 * log(10,000) ≈ 10,000 * 14 = 140,000 operations, efficient for interviews.
Interview Verdict: Accepted

This is the optimal and most elegant approach, demonstrating mastery of graph algorithms and priority queues.

📊
All Approaches - One-Glance Tradeoffs
💡 In interviews, coding the Dijkstra variant is usually best; binary search + BFS is a good alternative. Brute force is only for explanation.
ApproachTimeSpaceStack RiskReconstructUse In Interview
1. Brute Force (DFS with Backtracking)O(4^(n*m))O(n*m)Yes (deep recursion)YesMention only - never code
2. Binary Search + BFSO(n*m*log(maxHeightDifference))O(n*m)NoNo (only feasibility check)Good alternative if Dijkstra is not known
3. Dijkstra’s Algorithm VariantO(n*m*log(n*m))O(n*m)NoYesOptimal approach to code in interviews
💼
Interview Strategy
💡 Use this guide to understand the problem deeply, practice all approaches, and know when and how to present each during interviews.

How to Present

Clarify the problem and constraints with the interviewer.Explain the brute force approach to show understanding.Discuss its inefficiency and introduce binary search + BFS as an optimization.Finally, present the Dijkstra variant as the optimal solution.Code the chosen approach carefully and test with examples.

Time Allocation

Clarify: 2min → Approach discussion: 5min → Code: 10min → Testing & optimization: 3min. Total ~20min

What the Interviewer Tests

Understanding of graph traversal, ability to optimize brute force, knowledge of binary search on answer, and mastery of Dijkstra’s algorithm variant.

Common Follow-ups

  • What if diagonal moves are allowed? → Adjust directions and algorithm accordingly.
  • Can you solve it using Union-Find? → Yes, by sorting edges and connecting components.
💡 These follow-ups test flexibility and knowledge of alternative graph algorithms.
🔍
Pattern Recognition

When to Use

1) Problem involves grid or graph paths, 2) Need to minimize maximum edge cost, 3) Classic shortest path sum doesn’t apply, 4) Constraints allow graph traversal with priority queue or binary search.

Signature Phrases

minimum maximum differencepath with minimum effortminimize the largest step

NOT This Pattern When

Classic shortest path problems minimizing sum of weights (e.g., Dijkstra standard) differ because they sum edges instead of minimizing max edge.

Similar Problems

Swim in Rising Water - similar binary search + BFS approachMinimum Cost to Reach Destination in Time - shortest path with constraints

Practice

(1/5)
1. Consider the following Python code implementing the Number of Islands problem using in-place DFS. Given the input grid below, what is the final returned value of numIslands(grid)?
grid = [
  ["1","1","0"],
  ["0","1","0"],
  ["1","0","1"]
]
easy
A. 1
B. 2
C. 4
D. 3

Solution

  1. Step 1: Trace DFS calls starting from (0,0)

    DFS marks (0,0), (0,1), and (1,1) as visited, sinking one island.
  2. Step 2: Identify remaining islands

    Next unvisited '1' is at (2,0) (second island), then at (2,2) (third island). Total islands counted = 3.
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    Counting connected components by DFS yields 3 islands [OK]
Hint: Count connected components by DFS marking visited [OK]
Common Mistakes:
  • Off-by-one counting islands
  • Missing diagonal connections (which don't count)
2. Given the following Python code snippet implementing the BFS approach for Pacific Atlantic Water Flow, and the input heights = [[1,2,2],[3,2,3],[2,4,5]], what is the value of pacific_reachable after initializing the queues and marking the border cells?
easy
A. [[True, True, True], [True, False, False], [True, False, False]]
B. [[True, True, True], [True, False, False], [True, False, True]]
C. [[True, True, True], [False, False, False], [True, False, False]]
D. [[True, False, True], [True, False, False], [True, False, False]]

Solution

  1. Step 1: Mark Pacific border cells

    Pacific borders are first row and first column. So pacific_reachable[0][j] and pacific_reachable[i][0] are True.
  2. Step 2: Fill pacific_reachable matrix

    For m=3, n=3, first row and first column cells are True, others False initially.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    First row and first column are True, rest False [OK]
Hint: Pacific reachable True on top row and left column [OK]
Common Mistakes:
  • Forgetting to mark all border cells
  • Marking wrong cells True
3. Suppose the graph edges can have negative weights but no negative cycles. Which algorithm modification or choice correctly computes the shortest network delay times from a start node?
hard
A. Use DFS with path relaxation and memoization to handle negative weights efficiently.
B. Use Dijkstra's algorithm with a priority queue as usual; it handles negative weights correctly.
C. Use BFS since negative weights can be ignored for shortest paths.
D. Use Bellman-Ford algorithm to relax edges repeatedly, detecting negative cycles if any.

Solution

  1. Step 1: Recognize that Dijkstra's algorithm fails with negative weights because it assumes non-negative edge costs

    BFS cannot handle weighted edges properly, and DFS with memoization is inefficient and may not terminate correctly.
  2. Step 2: Bellman-Ford algorithm is designed to handle negative weights and detect negative cycles by repeated edge relaxation

    It guarantees correct shortest paths if no negative cycles exist.
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    Bellman-Ford handles negative weights safely [OK]
Hint: Bellman-Ford handles negative weights; Dijkstra does not [OK]
Common Mistakes:
  • Using Dijkstra with negative weights incorrectly
  • Assuming BFS works for weighted graphs
  • Believing DFS with memoization is efficient here
4. Suppose the graph edges can be added or removed dynamically, and you need to frequently query the number of connected components. Which data structure or approach is best suited to efficiently handle this scenario?
hard
A. Use a Union-Find (Disjoint Set Union) data structure with path compression
B. Store adjacency matrix and count components by matrix multiplication
C. Use DFS with recursion for each query to count components
D. Run BFS from scratch on the entire graph for each query

Solution

  1. Step 1: Understand dynamic edge updates require efficient union and find operations

    Union-Find supports near O(1) union and find operations, ideal for dynamic connectivity.
  2. Step 2: Compare BFS/DFS which require full traversal each query

    Re-running BFS/DFS each query is O(n + e), inefficient for frequent updates.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Union-Find is standard for dynamic connectivity [OK]
Hint: Union-Find excels at dynamic connectivity queries [OK]
Common Mistakes:
  • Re-running BFS/DFS each query
  • Using adjacency matrix which is costly
5. Suppose the Word Ladder problem is modified so that words can be reused multiple times in the transformation sequence. Which of the following changes to the bidirectional BFS algorithm is necessary to correctly handle this variant?
hard
A. Allow revisiting words but track path length to avoid infinite loops
B. Remove the visited set entirely to allow revisiting words
C. Use a priority queue to prioritize words closer to the endWord
D. Keep visited sets but reset them after each level expansion

Solution

  1. Step 1: Understand impact of allowing word reuse

    Allowing reuse means cycles can form, so simply removing visited causes infinite loops.
  2. Step 2: Modify algorithm to track path length and avoid infinite loops

    We must allow revisits but prevent infinite cycles by tracking path length or depth limits.
  3. Step 3: Why other options fail

    Removing visited causes infinite loops; resetting visited loses progress; priority queue is unrelated to reuse handling.
  4. Final Answer:

    Option A -> Option A
  5. Quick Check:

    Tracking path length prevents infinite loops with reuse [OK]
Hint: Allow reuse -> track path length to avoid cycles [OK]
Common Mistakes:
  • Removing visited causes infinite loops
  • Resetting visited loses pruning
  • Using priority queue doesn't solve reuse
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard