1102. Path With Maximum Minimum Value π
Problem Description
You are given an m x n
integer matrix grid
. Your task is to find the maximum score of any path from the top-left corner (0, 0)
to the bottom-right corner (m - 1, n - 1)
.
You can move in 4 cardinal directions: up, down, left, or right.
The score of a path is defined as the minimum value among all cells in that path. For example, if you traverse through cells with values 8 β 4 β 5 β 9
, the score of this path would be 4
(the minimum value).
Your goal is to find the path that maximizes this minimum value - in other words, you want to find the path where the smallest number you encounter is as large as possible.
The solution uses a clever approach combining sorting and Union-Find. It sorts all cells by their values in descending order, then progressively adds cells from highest to lowest value. As cells are added, they are connected to their already-visited neighbors using Union-Find. The process continues until the start and end positions become connected, at which point the last added cell's value represents the maximum possible minimum value for any path between start and end.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem involves traversing a 2D grid where each cell can be considered a node, and movements between adjacent cells (up, down, left, right) represent edges. We need to find a path from one node (0,0) to another (m-1, n-1).
Is it a tree?
- No: The grid structure allows multiple paths between nodes and can contain cycles (you can move in 4 directions), so it's not a tree structure.
Is the problem related to directed acyclic graphs?
- No: The grid allows bidirectional movement between cells, and cycles are possible, so it's not a DAG.
Is the problem related to shortest paths?
- No: We're not looking for the shortest path. Instead, we want to maximize the minimum value along the path, which is a different optimization criterion.
Does the problem involve connectivity?
- Yes: We need to determine if and when the start position (0,0) becomes connected to the end position (m-1, n-1) through visited cells. The solution progressively adds cells and checks when these two positions become connected.
Disjoint Set Union
- Yes: The solution uses DSU (Union-Find) to efficiently track and merge connected components as cells are added in descending order of their values.
Conclusion: While the flowchart leads us to DSU (which the solution indeed uses), the problem can also be solved using DFS with binary search or a modified Dijkstra's algorithm. The DSU approach is particularly elegant here because it allows us to process cells from highest to lowest value and find the exact moment when start and end become connected, giving us the maximum possible minimum value.
Intuition
Think about this problem in reverse. Instead of finding a path and calculating its minimum value, what if we start with the highest possible values and work our way down?
Imagine you're building a path by only using cells with values greater than or equal to some threshold k
. If you can connect the start to the end using only these high-value cells, then k
is a valid answer. The key insight is that the maximum possible k
is the answer we're looking for.
Now, how do we find this maximum k
efficiently? We can process cells in descending order of their values. Start with the highest value cells and progressively add lower value cells. Each time we add a cell, we check if it connects to any previously added adjacent cells. We keep track of connected components using Union-Find.
The moment we add a cell that causes the start position (0, 0)
and end position (m-1, n-1)
to become connected, that cell's value is our answer. Why? Because this is the minimum value we had to include to establish connectivity - any higher threshold would leave start and end disconnected.
This approach is like gradually "flooding" the grid with cells from highest to lowest value, watching for when the flood creates a bridge between start and end. The value at which this bridge forms is the maximum minimum value possible for any path.
The Union-Find data structure is perfect here because it efficiently handles:
- Merging adjacent visited cells into connected components
- Quickly checking if two positions belong to the same component
This transforms a complex path-finding problem into a simpler connectivity problem solved by processing cells in the right order.
Learn more about Depth-First Search, Breadth-First Search, Union Find, Binary Search and Heap (Priority Queue) patterns.
Solution Approach
The solution implements the sorting + Union-Find approach to find the maximum minimum value path:
1. Data Structure Setup:
- Create a Union-Find parent array
p
of sizem * n
where each cell is initially its own parent - Each cell
(i, j)
is mapped to a unique indexi * n + j
in the parent array
2. Prepare and Sort Cells:
- Create a list
q
containing triplets(v, i, j)
for each cell, wherev
is the cell value and(i, j)
are coordinates - Sort
q
in ascending order by value (we'll pop from the end to process in descending order)
3. Process Cells in Descending Order:
while find(0) != find(m * n - 1): v, i, j = q.pop() # Get cell with highest unprocessed value ans = v # Update answer to current cell value vis.add((i, j)) # Mark cell as visited
4. Connect Adjacent Visited Cells:
- For each newly visited cell, check its 4 adjacent positions using direction vectors
dirs = (-1, 0, 1, 0, -1)
- If an adjacent cell
(x, y)
has already been visited, union the current cell with it:
if (x, y) in vis: p[find(i * n + j)] = find(x * n + y)
5. Union-Find Operations:
- The
find
function uses path compression to efficiently find the root of a component:
def find(x: int) -> int:
if p[x] != x:
p[x] = find(p[x]) # Path compression
return p[x]
6. Termination Condition:
- The loop continues until
find(0) != find(m * n - 1)
becomes false - This means the start cell
(0, 0)
and end cell(m-1, n-1)
are now in the same connected component - The last processed cell value
ans
is the maximum minimum value for any path
The algorithm cleverly avoids explicit path finding by recognizing that if we can only use cells with values β₯ k
, then k
is achievable if and only if there exists a connected path using those cells. By processing cells from highest to lowest value, we find the exact threshold where connectivity is established.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Consider this 3Γ3 grid:
[5, 4, 5] [1, 6, 1] [7, 8, 9]
Our goal is to find a path from (0,0) to (2,2) where the minimum value along the path is maximized.
Step 1: Setup
- Create parent array
p = [0,1,2,3,4,5,6,7,8]
(each cell is its own parent) - Map positions: (0,0)β0, (0,1)β1, (0,2)β2, (1,0)β3, (1,1)β4, (1,2)β5, (2,0)β6, (2,1)β7, (2,2)β8
Step 2: Create and Sort Cell List Create list of (value, i, j) triplets and sort by value:
Sorted (ascending): [(1,1,0), (1,1,2), (4,0,1), (5,0,0), (5,0,2), (6,1,1), (7,2,0), (8,2,1), (9,2,2)]
We'll process from the end (highest values first).
Step 3: Process Cells in Descending Order
Iteration 1: Pop (9,2,2) - value 9
- Mark (2,2) as visited
- Check neighbors: none visited yet
- Start (0,0) and end (2,2) not connected
- Continue
Iteration 2: Pop (8,2,1) - value 8
- Mark (2,1) as visited
- Check neighbors: (2,2) is visited
- Union them: connect components containing positions 7 and 8
- Start and end still not connected
Iteration 3: Pop (7,2,0) - value 7
- Mark (2,0) as visited
- Check neighbors: (2,1) is visited
- Union them: connect components containing positions 6 and 7
- Now cells (2,0), (2,1), (2,2) are connected
- Start and end still not connected
Iteration 4: Pop (6,1,1) - value 6
- Mark (1,1) as visited
- Check neighbors: (2,1) is visited
- Union them: connect components containing positions 4 and 7
- Now (1,1) connects to the bottom row
- Start and end still not connected
Iteration 5: Pop (5,0,2) - value 5
- Mark (0,2) as visited
- Check neighbors: (1,2) not visited yet
- Start and end still not connected
Iteration 6: Pop (5,0,0) - value 5
- Mark (0,0) as visited (this is our start!)
- Check neighbors: (0,1) not visited, (1,0) not visited
- Start and end still not connected
Iteration 7: Pop (4,0,1) - value 4
- Mark (0,1) as visited
- Check neighbors: (0,0) and (0,2) are visited, (1,1) is visited
- Union (0,1) with (0,0): positions 1 and 0 connected
- Union (0,1) with (0,2): positions 1 and 2 connected
- Union (0,1) with (1,1): positions 1 and 4 connected
- Now find(0) == find(8) - Start and end are connected!
- Answer = 4
The algorithm found that 4 is the maximum minimum value. Indeed, the path (0,0)β(0,1)β(1,1)β(2,1)β(2,2) has values 5β4β6β8β9, with minimum value 4. No path exists where all values are β₯ 5.
Solution Implementation
1class Solution:
2 def maximumMinimumPath(self, grid: List[List[int]]) -> int:
3 # Find operation with path compression for Union-Find
4 def find(node: int) -> int:
5 if parent[node] != node:
6 parent[node] = find(parent[node]) # Path compression
7 return parent[node]
8
9 rows, cols = len(grid), len(grid[0])
10
11 # Initialize parent array for Union-Find
12 # Each cell (i, j) is mapped to index i * cols + j
13 parent = list(range(rows * cols))
14
15 # Create list of (value, row, col) tuples and sort by value
16 cells = [(value, row, col)
17 for row, row_values in enumerate(grid)
18 for col, value in enumerate(row_values)]
19 cells.sort() # Sort in ascending order
20
21 result = 0
22 # Direction vectors for exploring 4 adjacent cells (up, right, down, left)
23 directions = (-1, 0, 1, 0, -1)
24 visited = set()
25
26 # Process cells from largest value to smallest
27 # Continue until start and end cells are connected
28 while find(0) != find(rows * cols - 1):
29 value, row, col = cells.pop() # Get cell with largest remaining value
30 result = value # Update minimum value on the path
31 visited.add((row, col))
32
33 # Check all 4 adjacent cells
34 for i in range(4):
35 next_row = row + directions[i]
36 next_col = col + directions[i + 1]
37
38 # If adjacent cell was already visited, union them
39 if (next_row, next_col) in visited:
40 # Union operation: connect current cell with adjacent cell
41 parent[find(row * cols + col)] = find(next_row * cols + next_col)
42
43 return result
44
1class Solution {
2 private int[] parent; // Union-Find parent array
3
4 public int maximumMinimumPath(int[][] grid) {
5 int rows = grid.length;
6 int cols = grid[0].length;
7
8 // Initialize Union-Find parent array
9 parent = new int[rows * cols];
10
11 // Create list to store all cells with their values and coordinates
12 List<int[]> cells = new ArrayList<>();
13
14 // Populate cells list and initialize Union-Find
15 for (int i = 0; i < rows; i++) {
16 for (int j = 0; j < cols; j++) {
17 // Store [value, row, col] for each cell
18 cells.add(new int[] {grid[i][j], i, j});
19 // Initially, each cell is its own parent
20 parent[i * cols + j] = i * cols + j;
21 }
22 }
23
24 // Sort cells in descending order by value
25 // Process cells from highest to lowest value
26 cells.sort((a, b) -> b[0] - a[0]);
27
28 // Track which cells have been visited/added to the graph
29 boolean[][] visited = new boolean[rows][cols];
30
31 // Direction vectors for 4-directional movement (up, right, down, left)
32 int[] directions = {-1, 0, 1, 0, -1};
33
34 int result = 0;
35
36 // Process cells until start and end are connected
37 for (int i = 0; find(0) != find(rows * cols - 1); i++) {
38 int[] currentCell = cells.get(i);
39 int value = currentCell[0];
40 int row = currentCell[1];
41 int col = currentCell[2];
42
43 // Mark current cell as visited
44 visited[row][col] = true;
45
46 // Update result with current cell's value
47 // Since we process in decreasing order, this will be the minimum
48 // value in the path when start and end finally connect
49 result = value;
50
51 // Try to connect with adjacent visited cells
52 for (int k = 0; k < 4; k++) {
53 int newRow = row + directions[k];
54 int newCol = col + directions[k + 1];
55
56 // Check if adjacent cell is valid and already visited
57 if (newRow >= 0 && newRow < rows &&
58 newCol >= 0 && newCol < cols &&
59 visited[newRow][newCol]) {
60
61 // Union the adjacent visited cell with current cell
62 parent[find(newRow * cols + newCol)] = find(row * cols + col);
63 }
64 }
65 }
66
67 return result;
68 }
69
70 // Find operation with path compression for Union-Find
71 private int find(int x) {
72 if (parent[x] != x) {
73 // Path compression: make x point directly to root
74 parent[x] = find(parent[x]);
75 }
76 return parent[x];
77 }
78}
79
1class Solution {
2public:
3 int maximumMinimumPath(vector<vector<int>>& grid) {
4 int rows = grid.size();
5 int cols = grid[0].size();
6
7 // Store all cells with their values and coordinates
8 vector<tuple<int, int, int>> cells;
9
10 // Union-Find parent array (using 1D indexing for 2D grid)
11 vector<int> parent(rows * cols);
12 iota(parent.begin(), parent.end(), 0); // Initialize each cell as its own parent
13
14 // Collect all cells with their values
15 for (int row = 0; row < rows; ++row) {
16 for (int col = 0; col < cols; ++col) {
17 cells.emplace_back(grid[row][col], row, col);
18 }
19 }
20
21 // Find operation with path compression
22 function<int(int)> find = [&](int x) {
23 if (parent[x] != x) {
24 parent[x] = find(parent[x]); // Path compression
25 }
26 return parent[x];
27 };
28
29 // Sort cells by value in descending order (process largest values first)
30 sort(cells.begin(), cells.end(), greater<tuple<int, int, int>>());
31
32 int result = 0;
33
34 // Direction vectors for 4-connected neighbors (up, right, down, left)
35 int directions[5] = {-1, 0, 1, 0, -1};
36
37 // Track visited cells
38 bool visited[rows][cols];
39 memset(visited, false, sizeof(visited));
40
41 // Process cells from largest to smallest value
42 for (auto& [value, row, col] : cells) {
43 visited[row][col] = true;
44 result = value; // Update result with current value
45
46 // Connect current cell with its visited neighbors
47 for (int dir = 0; dir < 4; ++dir) {
48 int newRow = row + directions[dir];
49 int newCol = col + directions[dir + 1];
50
51 // Check if neighbor is valid and already visited
52 if (newRow >= 0 && newRow < rows &&
53 newCol >= 0 && newCol < cols &&
54 visited[newRow][newCol]) {
55
56 // Union the two cells (connect their components)
57 int neighborIndex = newRow * cols + newCol;
58 int currentIndex = row * cols + col;
59 parent[find(neighborIndex)] = find(currentIndex);
60 }
61 }
62
63 // Check if start (0,0) and end (m-1,n-1) are connected
64 if (find(0) == find(rows * cols - 1)) {
65 break; // Path found, current value is the maximum minimum
66 }
67 }
68
69 return result;
70 }
71};
72
1/**
2 * Finds the maximum minimum path value from top-left to bottom-right corner of the grid.
3 * Uses Union-Find (Disjoint Set Union) to connect cells when they are visited.
4 * @param grid - 2D array of numbers representing the grid
5 * @returns The maximum score of the minimum value in any path
6 */
7function maximumMinimumPath(grid: number[][]): number {
8 const rows: number = grid.length;
9 const cols: number = grid[0].length;
10
11 // Initialize parent array for Union-Find structure
12 // Each cell is initially its own parent
13 const parent: number[] = Array(rows * cols)
14 .fill(0)
15 .map((_, index) => index);
16
17 // Create array of cells sorted by their values in descending order
18 const cellsWithValues: number[][] = [];
19 for (let i = 0; i < rows; ++i) {
20 for (let j = 0; j < cols; ++j) {
21 cellsWithValues.push([grid[i][j], i, j]);
22 }
23 }
24 cellsWithValues.sort((a, b) => b[0] - a[0]);
25
26 /**
27 * Find operation with path compression for Union-Find
28 * @param x - Node to find root for
29 * @returns Root of the set containing x
30 */
31 const findRoot = (x: number): number => {
32 if (parent[x] !== x) {
33 parent[x] = findRoot(parent[x]); // Path compression
34 }
35 return parent[x];
36 };
37
38 // Direction vectors for exploring 4 adjacent cells (up, right, down, left)
39 const directions: number[] = [-1, 0, 1, 0, -1];
40
41 // Track visited cells
42 const visited: boolean[][] = Array(rows)
43 .fill(0)
44 .map(() => Array(cols).fill(false));
45
46 let result: number = 0;
47
48 // Process cells from highest to lowest value
49 // Continue until start and end cells are connected
50 for (let k = 0; findRoot(0) !== findRoot(rows * cols - 1); ++k) {
51 const [currentValue, row, col] = cellsWithValues[k];
52 result = currentValue;
53 visited[row][col] = true;
54
55 // Check all 4 adjacent cells
56 for (let d = 0; d < 4; ++d) {
57 const newRow: number = row + directions[d];
58 const newCol: number = col + directions[d + 1];
59
60 // If adjacent cell is valid and already visited, union them
61 if (newRow >= 0 && newRow < rows &&
62 newCol >= 0 && newCol < cols &&
63 visited[newRow][newCol]) {
64 // Union operation: connect current cell with adjacent visited cell
65 parent[findRoot(row * cols + col)] = findRoot(newRow * cols + newCol);
66 }
67 }
68 }
69
70 return result;
71}
72
Time and Space Complexity
Time Complexity: O(m Γ n Γ (log(m Γ n) + Ξ±(m Γ n)))
The time complexity breaks down as follows:
- Creating the list
q
with all grid elements takesO(m Γ n)
time - Sorting the list
q
containingm Γ n
elements takesO(m Γ n Γ log(m Γ n))
time - The while loop processes at most
m Γ n
elements from the sorted list - For each element processed, we:
- Perform union-find operations with
find()
calls, which takeO(Ξ±(m Γ n))
amortized time whereΞ±
is the inverse Ackermann function - Check up to 4 neighboring cells and potentially perform union operations
- Perform union-find operations with
- The total union-find operations across all iterations contribute
O(m Γ n Γ Ξ±(m Γ n))
to the complexity - The dominant term is the sorting operation, giving us
O(m Γ n Γ log(m Γ n))
, but when combined with the union-find operations throughout the algorithm, the overall complexity becomesO(m Γ n Γ (log(m Γ n) + Ξ±(m Γ n)))
Space Complexity: O(m Γ n)
The space complexity consists of:
- The parent array
p
storingm Γ n
elements:O(m Γ n)
- The list
q
storing tuples for allm Γ n
grid cells:O(m Γ n)
- The visited set
vis
can contain at mostm Γ n
coordinate pairs:O(m Γ n)
- The recursion stack for
find()
operations:O(log(m Γ n))
in the average case with path compression - Overall space complexity is
O(m Γ n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Union Operation
A critical pitfall is performing the union operation incorrectly by directly connecting nodes instead of their root representatives.
Incorrect Implementation:
# Wrong: Directly connecting nodes without finding roots parent[row * cols + col] = next_row * cols + next_col
Correct Implementation:
# Right: Connect the root of one component to the root of another parent[find(row * cols + col)] = find(next_row * cols + next_col)
2. Boundary Check Omission
Failing to validate that adjacent cells are within grid boundaries before checking if they're visited.
Problematic Code:
for i in range(4):
next_row = row + directions[i]
next_col = col + directions[i + 1]
# Missing boundary check!
if (next_row, next_col) in visited: # This could check invalid coordinates
parent[find(row * cols + col)] = find(next_row * cols + next_col)
Fixed Version:
for i in range(4):
next_row = row + directions[i]
next_col = col + directions[i + 1]
# Add boundary validation
if (0 <= next_row < rows and 0 <= next_col < cols and
(next_row, next_col) in visited):
parent[find(row * cols + col)] = find(next_row * cols + next_col)
3. Processing Order Confusion
Processing cells in ascending order instead of descending order, which would find the minimum maximum value instead of the maximum minimum value.
Wrong Approach:
cells.sort() # Ascending order for value, row, col in cells: # Process from smallest to largest # This finds when disconnection happens, not connection
Correct Approach:
cells.sort() # Sort ascending while condition: value, row, col = cells.pop() # Pop from end (largest values first)
4. Forgetting Path Compression
Implementing find() without path compression leads to degraded performance from O(Ξ±(n)) to O(n) per operation.
Inefficient Version:
def find(x: int) -> int:
while parent[x] != x:
x = parent[x]
return x
Optimized Version:
def find(x: int) -> int:
if parent[x] != x:
parent[x] = find(parent[x]) # Path compression
return parent[x]
5. Misunderstanding the Answer Update
Updating the answer only once at the end or incorrectly tracking the minimum value.
Incorrect:
# Wrong: Using the first or last processed value return cells[-1][0] # This would be the smallest value
Correct:
# Right: The answer is the value of the last cell processed # before start and end become connected while find(0) != find(rows * cols - 1): value, row, col = cells.pop() result = value # Continuously update with current value
How does merge sort divide the problem into subproblems?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Want a Structured Path to Master System Design Too? Donβt Miss This!