778. Swim In Rising Water - Explanation

Problem Link

Description

You are given a square 2-D matrix of distinct integers grid where each integer grid[i][j] represents the elevation at position (i, j).

Rain starts to fall at time = 0, which causes the water level to rise. At time t, the water level across the entire grid is t.

You may swim either horizontally or vertically in the grid between two adjacent squares if the original elevation of both squares is less than or equal to the water level at time t.

Starting from the top left square (0, 0), return the minimum amount of time it will take until it is possible to reach the bottom right square (n - 1, n - 1).

Example 1:

Input: grid = [[0,1],[2,3]]

Output: 3

Explanation: For a path to exist to the bottom right square grid[1][1] the water elevation must be at least 3. At time t = 3, the water level is 3.

Example 2:

Input: grid = [
  [0,1,2,10],
  [9,14,4,13],
  [12,3,8,15],
  [11,5,7,6]
]

Output: 8

Explanation: The water level must be at least 8 to reach the bottom right square. The path is [0, 1, 2, 4, 8, 7, 6].

Constraints:

  • grid.length == grid[i].length
  • 1 <= grid.length <= 50
  • 0 <= grid[i][j] < n^2


Topics

Recommended Time & Space Complexity

You should aim for a solution with O((n^2)logn) time and O(n^2) space, where n is the number of rows or columns of the square matrix.


Hint 1

Think of this problem as a graph where each cell represents a node. You can move from one cell to its adjacent cell if the time is greater than or equal to the adjacent cell's elevation. Note that swimming does not cost time, but you may need to wait at a cell for the time to reach the required elevation. What do you notice about the path from (0, 0) to (n - 1, n - 1)? Perhaps a greedy approach would be useful here.


Hint 2

We can observe that the maximum elevation value along the path determines the time taken for that path. Therefore, we need to find the path where the maximum elevation is minimized. Can you think of an algorithm to find such a path from the source (0, 0) to the destination (n - 1, n - 1)? Perhaps a shortest path algorithm could be useful here.


Hint 3

We can use Dijkstra's algorithm. We initialize a Min-heap and a matrix with infinity. We run the algorithm starting from the source (0, 0), and we track the maximum elevation encountered along the paths. This maximum elevation is used as the key for comparison in Dijkstra's algorithm. If we encounter the destination (n - 1, n - 1), we return the maximum elevation of the path that reached the destination.


Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Graph Traversal (DFS/BFS) - Exploring grid cells using depth-first or breadth-first search with 4-directional movement
  • Binary Search - Using binary search on the answer to optimize searching for the minimum valid time
  • Dijkstra's Algorithm - Finding shortest paths with modified cost function (minimizing maximum edge weight)
  • Union-Find (Disjoint Set Union) - Tracking connected components as cells become accessible over time
  • Priority Queue / Min-Heap - Efficiently selecting the next cell with minimum cost in greedy approaches

1. Brute Force

Intuition

This brute force tries every possible path from the top-left to the bottom-right.

While walking, your “time” is not the number of steps — it’s the maximum height value you have stepped on so far, because you must wait until water rises to at least that height to pass those cells.

So for each path:

  • The cost of the path = max cell value on that path
  • We want the path with the minimum such cost (minimize the worst height you had to cross).

The DFS explores all routes, keeps a visited grid to avoid looping, and returns the best (minimum) possible maximum-height value among all paths.

Algorithm

  1. Start dfs from (0, 0) with initial time t = 0.
  2. For a cell (r, c):
    • If out of bounds or already visited → return a very large number (invalid path).
    • Update t = max(t, grid[r][c]) (time needed to stand on this cell).
    • If (r, c) is the destination (n-1, n-1) → return t.
  3. Mark (r, c) as visited.
  4. Recursively try all 4 directions (up, down, left, right).
  5. Take the minimum result among the 4 recursive calls (best path from here).
  6. Unmark (r, c) (backtrack) and return that minimum.
class Solution:
    def swimInWater(self, grid: List[List[int]]) -> int:
        n = len(grid)
        visit = [[False] * n for _ in range(n)]

        def dfs(node, t):
            r, c = node
            if min(r, c) < 0 or max(r, c) >= n or visit[r][c]:
                return 1000000
            if r == (n - 1) and c == (n - 1):
                return max(t, grid[r][c])
            visit[r][c] = True
            t = max(t, grid[r][c])
            res = min(dfs((r + 1, c), t),
                       dfs((r - 1, c), t),
                       dfs((r, c + 1), t),
                       dfs((r, c - 1), t))
            visit[r][c] = False
            return res

        return dfs((0, 0), 0)

Time & Space Complexity

  • Time complexity: O(4n2)O(4 ^ {n ^ 2})
  • Space complexity: O(n2)O(n ^ 2)

Intuition

Here we turn the problem into a yes/no question:

“If the water level is t, can I reach the bottom-right?”

At water level t, you are only allowed to step on cells with grid[r][c] <= t.
So we try a DFS that only walks through “allowed” cells.
Then we increase t gradually from the smallest possible height to the largest, and return the first t where reaching the end becomes possible.

Algorithm

  1. Compute:
    • minH = minimum value in the grid (smallest possible water level to consider)
    • maxH = maximum value in the grid (guaranteed enough water level)
  2. Define canReach(t) using dfs:
    • Start from (0,0)
    • You can move 4 directions.
    • You cannot enter a cell if:
      • it is out of bounds, or
      • already visited, or
      • its height > t (not flooded enough yet)
    • If you reach (n-1, n-1), return true.
  3. For t from minH to maxH:
    • If canReach(t) is true, return t (first possible time).
    • Otherwise reset visited and try the next t.
  4. If none worked earlier, return maxH.
class Solution:
    def swimInWater(self, grid: List[List[int]]) -> int:
        n = len(grid)
        visit = [[False] * n for _ in range(n)]
        minH = maxH = grid[0][0]
        for row in range(n):
            maxH = max(maxH, max(grid[row]))
            minH = min(minH, min(grid[row]))

        def dfs(node, t):
            r, c = node
            if (min(r, c) < 0 or max(r, c) >= n or
                visit[r][c] or grid[r][c] > t):
                return False
            if r == (n - 1) and c == (n - 1):
                return True
            visit[r][c] = True
            return (dfs((r + 1, c), t) or
                    dfs((r - 1, c), t) or
                    dfs((r, c + 1), t) or
                    dfs((r, c - 1), t))

        for t in range(minH, maxH):
            if dfs((0, 0), t):
                return t
            for r in range(n):
                for c in range(n):
                    visit[r][c] = False

        return maxH

Time & Space Complexity

  • Time complexity: O(n4)O(n ^ 4)
  • Space complexity: O(n2)O(n ^ 2)

3. Binary Search + DFS

Intuition

Instead of trying every water level t one by one, we binary search the answer.

Key idea:

  • If you can reach the bottom-right at water level t, then you can also reach it at any higher level (t+1, t+2, ...).
  • If you cannot reach at t, you also cannot reach at any lower level.

So the condition “reachable at t” is monotonic → perfect for binary search.

To test a fixed t, run a DFS from (0,0) and only move through cells with height <= t.

Algorithm

  1. Compute the search range:
    • low = minimum height in grid
    • high = maximum height in grid
  2. Define canReach(t) using dfs:
    • Start at (0,0)
    • Move in 4 directions.
    • Reject moves that are:
      • out of bounds, or
      • already visited, or
      • on a cell with grid[r][c] > t
    • If dfs reaches (n-1, n-1), return true, else false.
  3. Binary search on t:
    • mid = (low + high) // 2
    • If canReach(mid) is true → try smaller time: high = mid
    • Else → need more water: low = mid + 1
    • Reset visited before the next dfs check.
  4. When low == high, that value is the minimum time needed.
class Solution:
    def swimInWater(self, grid: List[List[int]]) -> int:
        n = len(grid)
        visit = [[False] * n for _ in range(n)]
        minH = maxH = grid[0][0]
        for row in range(n):
            maxH = max(maxH, max(grid[row]))
            minH = min(minH, min(grid[row]))

        def dfs(node, t):
            r, c = node
            if (min(r, c) < 0 or max(r, c) >= n or
                visit[r][c] or grid[r][c] > t):
                return False
            if r == (n - 1) and c == (n - 1):
                return True
            visit[r][c] = True
            return (dfs((r + 1, c), t) or
                    dfs((r - 1, c), t) or
                    dfs((r, c + 1), t) or
                    dfs((r, c - 1), t))

        l, r = minH, maxH
        while l < r:
            m = (l + r) >> 1
            if dfs((0, 0), m):
                r = m
            else:
                l = m + 1
            for row in range(n):
                for col in range(n):
                    visit[row][col] = False

        return r

Time & Space Complexity

  • Time complexity: O(n2logn)O(n ^ 2 \log n)
  • Space complexity: O(n2)O(n ^ 2)

4. Dijkstra's Algorithm

Intuition

Think of each cell’s height as the earliest time you’re allowed to stand on it (water must be at least that high).
While moving from start to end, the total time of a path is not the sum — it’s the maximum height you ever step on (because water must rise to that max).

So the problem becomes:

Find a path from (0,0) to (n-1,n-1) that minimizes the maximum cell height along the path.

That is exactly what Dijkstra can solve if we define:

  • cost to reach a cell = smallest possible “maximum height so far”.

Algorithm

  1. Use a min-heap storing states: (timeSoFar, r, c), where timeSoFar = max height on the path up to (r,c).
  2. Start by pushing the start cell: (grid[0][0], 0, 0).
  3. Repeatedly:
    • Pop the cell with the smallest timeSoFar.
    • If it's the destination, return timeSoFar (this is optimal).
    • For each of 4 neighbors:
      • If valid and not visited, compute:
        • newTime = max(timeSoFar, grid[nr][nc])
      • Push (newTime, nr, nc) into the heap.
  4. Use a visited set so each cell is processed once (when it's popped with the best possible timeSoFar).
class Solution:
    def swimInWater(self, grid: List[List[int]]) -> int:
        N = len(grid)
        visit = set()
        minH = [[grid[0][0], 0, 0]]  # (time/max-height, r, c)
        directions = [[0, 1], [0, -1], [1, 0], [-1, 0]]

        visit.add((0, 0))
        while minH:
            t, r, c = heapq.heappop(minH)
            if r == N - 1 and c == N - 1:
                return t
            for dr, dc in directions:
                neiR, neiC = r + dr, c + dc
                if (neiR < 0 or neiC < 0 or
                    neiR == N or neiC == N or
                    (neiR, neiC) in visit
                ):
                    continue
                visit.add((neiR, neiC))
                heapq.heappush(minH, [max(t, grid[neiR][neiC]), neiR, neiC])

Time & Space Complexity

  • Time complexity: O(n2logn)O(n ^ 2 \log n)
  • Space complexity: O(n2)O(n ^ 2)

5. Kruskal's Algorithm

Intuition

Water level t rises over time. At time t, you’re allowed to step only on cells with height <= t.
So as t increases, more cells become “open” and neighboring open cells form bigger connected regions.

We want the earliest time t when the start cell (0,0) and end cell (N-1,N-1) become part of the same connected component.

DSU (Union-Find) is perfect for this: it quickly merges neighboring open cells and checks if start and end are connected.

Algorithm (Kruskal-style using DSU)

  1. Create a list of all cells as (height, r, c) and sort by height (smallest first).
  2. Initialize dsu for N*N cells (each cell is a node with id = r*N + c).
  3. Process cells in increasing height order:
    • Current cell (r,c) becomes "open" at time t = height.
    • For each of its 4 neighbors:
      • If the neighbor's height <= t, it is already open (or also open now), so union the two cells.
    • After unions, check if start (0) and end (N*N-1) are connected.
    • The first t where they connect is the answer.
  4. Return that t.
class DSU:
    def __init__(self, n):
        self.Parent = list(range(n + 1))
        self.Size = [1] * (n + 1)

    def find(self, node):
        if self.Parent[node] != node:
            self.Parent[node] = self.find(self.Parent[node])
        return self.Parent[node]

    def union(self, u, v):
        pu = self.find(u)
        pv = self.find(v)
        if pu == pv:
            return False
        if self.Size[pu] < self.Size[pv]:
            pu, pv = pv, pu
        self.Size[pu] += self.Size[pv]
        self.Parent[pv] = pu
        return True

    def connected(self, u, v):
        return self.find(u) == self.find(v)

class Solution:
    def swimInWater(self, grid: List[List[int]]) -> int:
        N = len(grid)
        dsu = DSU(N * N)
        positions = sorted((grid[r][c], r, c) for r in range(N) for c in range(N))
        directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]

        for t, r, c in positions:
            for dr, dc in directions:
                nr, nc = r + dr, c + dc
                if 0 <= nr < N and 0 <= nc < N and grid[nr][nc] <= t:
                    dsu.union(r * N + c, nr * N + nc)
            if dsu.connected(0, N * N - 1):
                return t

Time & Space Complexity

  • Time complexity: O(n2logn)O(n ^ 2 \log n)
  • Space complexity: O(n2)O(n ^ 2)

Common Pitfalls

Confusing Time with Step Count

A common mistake is treating "time" as the number of steps taken. In this problem, time represents the maximum height you must wait for the water to rise to before you can traverse your chosen path. It is not the path length.

Forgetting to Include Start and End Cells

When calculating the minimum time, you must include both grid[0][0] and grid[n-1][n-1] in your considerations. The answer is at least max(grid[0][0], grid[n-1][n-1]) since you must be able to stand on both endpoints.

Incorrect Binary Search Bounds

When using binary search, setting the wrong initial bounds can cause issues. The lower bound should be the minimum value in the grid (or at least grid[0][0]), and the upper bound should be the maximum value. Using 0 to n*n-1 works but is less precise.

Not Resetting Visited Array Between Searches

In both the linear search and binary search DFS approaches, forgetting to reset the visited array between different attempts (for different time values) leads to incorrect results. Each DFS with a new threshold needs a fresh visited state.

Union-Find: Incorrect Node Indexing

When using Kruskal's algorithm with DSU, a frequent bug is incorrectly converting 2D coordinates to 1D indices. The formula r * N + c must be used consistently, and you must ensure neighbors are already "open" (have height <= current time) before attempting to union them.