Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Merging adjacent visited cells into connected components
  2. 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 size m * n where each cell is initially its own parent
  • Each cell (i, j) is mapped to a unique index i * n + j in the parent array

2. Prepare and Sort Cells:

  • Create a list q containing triplets (v, i, j) for each cell, where v 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 Evaluator

Example 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 takes O(m Γ— n) time
  • Sorting the list q containing m Γ— n elements takes O(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 take O(Ξ±(m Γ— n)) amortized time where Ξ± is the inverse Ackermann function
    • Check up to 4 neighboring cells and potentially perform union operations
  • 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 becomes O(m Γ— n Γ— (log(m Γ— n) + Ξ±(m Γ— n)))

Space Complexity: O(m Γ— n)

The space complexity consists of:

  • The parent array p storing m Γ— n elements: O(m Γ— n)
  • The list q storing tuples for all m Γ— n grid cells: O(m Γ— n)
  • The visited set vis can contain at most m Γ— 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How does merge sort divide the problem into subproblems?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More