Off-by-one on range boundaries
Wrong move: Loop endpoints miss first/last candidate.
Usually fails on: Fails on minimal arrays and exact-boundary answers.
Fix: Re-derive loops from inclusive/exclusive ranges before coding.
Move from brute-force thinking to an efficient approach using array strategy.
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.
Example 1:
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.
Example 2:
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].
Example 3:
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.
Constraints:
rows == heights.lengthcolumns == heights[i].length1 <= rows, columns <= 1001 <= heights[i][j] <= 106Problem summary: 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.
Start with the most direct exhaustive search. That gives a correctness anchor before optimizing.
Pattern signal: Array · Binary Search · Union-Find
[[1,2,2],[3,8,2],[5,3,5]]
[[1,2,3],[3,8,4],[5,3,5]]
[[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]]
swim-in-rising-water)path-with-maximum-minimum-value)find-the-safest-path-in-a-grid)Source-backed implementations are provided below for direct study and interview prep.
// Accepted solution for LeetCode #1631: Path With Minimum Effort
class UnionFind {
private final int[] p;
private final int[] size;
public UnionFind(int n) {
p = new int[n];
size = new int[n];
for (int i = 0; i < n; ++i) {
p[i] = i;
size[i] = 1;
}
}
public int find(int x) {
if (p[x] != x) {
p[x] = find(p[x]);
}
return p[x];
}
public boolean union(int a, int b) {
int pa = find(a), pb = find(b);
if (pa == pb) {
return false;
}
if (size[pa] > size[pb]) {
p[pb] = pa;
size[pa] += size[pb];
} else {
p[pa] = pb;
size[pb] += size[pa];
}
return true;
}
public boolean connected(int a, int b) {
return find(a) == find(b);
}
}
class Solution {
public int minimumEffortPath(int[][] heights) {
int m = heights.length, n = heights[0].length;
UnionFind uf = new UnionFind(m * n);
List<int[]> edges = new ArrayList<>();
int[] dirs = {1, 0, 1};
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
for (int k = 0; k < 2; ++k) {
int x = i + dirs[k], y = j + dirs[k + 1];
if (x >= 0 && x < m && y >= 0 && y < n) {
int d = Math.abs(heights[i][j] - heights[x][y]);
edges.add(new int[] {d, i * n + j, x * n + y});
}
}
}
}
Collections.sort(edges, (a, b) -> a[0] - b[0]);
for (int[] e : edges) {
uf.union(e[1], e[2]);
if (uf.connected(0, m * n - 1)) {
return e[0];
}
}
return 0;
}
}
// Accepted solution for LeetCode #1631: Path With Minimum Effort
type unionFind struct {
p, size []int
}
func newUnionFind(n int) *unionFind {
p := make([]int, n)
size := make([]int, n)
for i := range p {
p[i] = i
size[i] = 1
}
return &unionFind{p, size}
}
func (uf *unionFind) find(x int) int {
if uf.p[x] != x {
uf.p[x] = uf.find(uf.p[x])
}
return uf.p[x]
}
func (uf *unionFind) union(a, b int) bool {
pa, pb := uf.find(a), uf.find(b)
if pa == pb {
return false
}
if uf.size[pa] > uf.size[pb] {
uf.p[pb] = pa
uf.size[pa] += uf.size[pb]
} else {
uf.p[pa] = pb
uf.size[pb] += uf.size[pa]
}
return true
}
func (uf *unionFind) connected(a, b int) bool {
return uf.find(a) == uf.find(b)
}
func minimumEffortPath(heights [][]int) int {
m, n := len(heights), len(heights[0])
edges := make([][3]int, 0, m*n*2)
dirs := [3]int{0, 1, 0}
for i, row := range heights {
for j, h := range row {
for k := 0; k < 2; k++ {
x, y := i+dirs[k], j+dirs[k+1]
if x >= 0 && x < m && y >= 0 && y < n {
edges = append(edges, [3]int{abs(h - heights[x][y]), i*n + j, x*n + y})
}
}
}
}
sort.Slice(edges, func(i, j int) bool { return edges[i][0] < edges[j][0] })
uf := newUnionFind(m * n)
for _, e := range edges {
uf.union(e[1], e[2])
if uf.connected(0, m*n-1) {
return e[0]
}
}
return 0
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
# Accepted solution for LeetCode #1631: Path With Minimum Effort
class UnionFind:
def __init__(self, n):
self.p = list(range(n))
self.size = [1] * n
def find(self, x):
if self.p[x] != x:
self.p[x] = self.find(self.p[x])
return self.p[x]
def union(self, a, b):
pa, pb = self.find(a), self.find(b)
if pa == pb:
return False
if self.size[pa] > self.size[pb]:
self.p[pb] = pa
self.size[pa] += self.size[pb]
else:
self.p[pa] = pb
self.size[pb] += self.size[pa]
return True
def connected(self, a, b):
return self.find(a) == self.find(b)
class Solution:
def minimumEffortPath(self, heights: List[List[int]]) -> int:
m, n = len(heights), len(heights[0])
uf = UnionFind(m * n)
e = []
dirs = (0, 1, 0)
for i in range(m):
for j in range(n):
for a, b in pairwise(dirs):
x, y = i + a, j + b
if 0 <= x < m and 0 <= y < n:
e.append(
(abs(heights[i][j] - heights[x][y]), i * n + j, x * n + y)
)
e.sort()
for h, a, b in e:
uf.union(a, b)
if uf.connected(0, m * n - 1):
return h
return 0
// Accepted solution for LeetCode #1631: Path With Minimum Effort
struct Solution;
use std::cmp::Reverse;
use std::collections::BinaryHeap;
impl Solution {
fn minimum_effort_path(heights: Vec<Vec<i32>>) -> i32 {
let n = heights.len();
let m = heights[0].len();
let mut visited: Vec<Vec<bool>> = vec![vec![false; m]; n];
let mut queue: BinaryHeap<(Reverse<i32>, usize, usize)> = BinaryHeap::new();
let mut res = 0;
queue.push((Reverse(0), 0, 0));
while let Some((Reverse(effort), i, j)) = queue.pop() {
res = res.max(effort);
if i == n - 1 && j == m - 1 {
break;
}
visited[i][j] = true;
if i > 0 && !visited[i - 1][j] {
let diff = heights[i][j] - heights[i - 1][j];
queue.push((Reverse(diff.abs()), i - 1, j));
}
if j > 0 && !visited[i][j - 1] {
let diff = heights[i][j] - heights[i][j - 1];
queue.push((Reverse(diff.abs()), i, j - 1));
}
if i + 1 < n && !visited[i + 1][j] {
let diff = heights[i][j] - heights[i + 1][j];
queue.push((Reverse(diff.abs()), i + 1, j));
}
if j + 1 < m && !visited[i][j + 1] {
let diff = heights[i][j] - heights[i][j + 1];
queue.push((Reverse(diff.abs()), i, j + 1));
}
}
res
}
}
#[test]
fn test() {
let heights = vec_vec_i32![[1, 2, 2], [3, 8, 2], [5, 3, 5]];
let res = 2;
assert_eq!(Solution::minimum_effort_path(heights), res);
let heights = vec_vec_i32![[1, 2, 3], [3, 8, 4], [5, 3, 5]];
let res = 1;
assert_eq!(Solution::minimum_effort_path(heights), res);
let heights = vec_vec_i32![
[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]
];
let res = 0;
assert_eq!(Solution::minimum_effort_path(heights), res);
}
// Accepted solution for LeetCode #1631: Path With Minimum Effort
class UnionFind {
private p: number[];
private size: number[];
constructor(n: number) {
this.p = Array.from({ length: n }, (_, i) => i);
this.size = Array(n).fill(1);
}
find(x: number): number {
if (this.p[x] !== x) {
this.p[x] = this.find(this.p[x]);
}
return this.p[x];
}
union(a: number, b: number): boolean {
const pa = this.find(a);
const pb = this.find(b);
if (pa === pb) {
return false;
}
if (this.size[pa] > this.size[pb]) {
this.p[pb] = pa;
this.size[pa] += this.size[pb];
} else {
this.p[pa] = pb;
this.size[pb] += this.size[pa];
}
return true;
}
connected(a: number, b: number): boolean {
return this.find(a) === this.find(b);
}
}
function minimumEffortPath(heights: number[][]): number {
const m = heights.length;
const n = heights[0].length;
const uf = new UnionFind(m * n);
const edges: number[][] = [];
const dirs = [1, 0, 1];
for (let i = 0; i < m; ++i) {
for (let j = 0; j < n; ++j) {
for (let k = 0; k < 2; ++k) {
const x = i + dirs[k];
const y = j + dirs[k + 1];
if (x >= 0 && x < m && y >= 0 && y < n) {
const d = Math.abs(heights[i][j] - heights[x][y]);
edges.push([d, i * n + j, x * n + y]);
}
}
}
}
edges.sort((a, b) => a[0] - b[0]);
for (const [h, a, b] of edges) {
uf.union(a, b);
if (uf.connected(0, m * n - 1)) {
return h;
}
}
return 0;
}
Use this to step through a reusable interview workflow for this problem.
Check every element from left to right until we find the target or exhaust the array. Each comparison is O(1), and we may visit all n elements, giving O(n). No extra space needed.
Each comparison eliminates half the remaining search space. After k comparisons, the space is n/2ᵏ. We stop when the space is 1, so k = log₂ n. No extra memory needed — just two pointers (lo, hi).
Review these before coding to avoid predictable interview regressions.
Wrong move: Loop endpoints miss first/last candidate.
Usually fails on: Fails on minimal arrays and exact-boundary answers.
Fix: Re-derive loops from inclusive/exclusive ranges before coding.
Wrong move: Setting `lo = mid` or `hi = mid` can stall and create an infinite loop.
Usually fails on: Two-element ranges never converge.
Fix: Use `lo = mid + 1` or `hi = mid - 1` where appropriate.