Check if There is a Valid Path in a Grid
MedPrompt
You are given an m x n grid. Each cell of grid represents a street. The street of grid[i][j] can be:
1which means a street connecting the left cell and the right cell.2which means a street connecting the upper cell and the lower cell.3which means a street connecting the left cell and the lower cell.4which means a street connecting the right cell and the lower cell.5which means a street connecting the left cell and the upper cell.6which means a street connecting the right cell and the upper cell.
You will initially start at the street of the upper-left cell (0, 0). A valid path in the grid is a path that starts from the upper left cell (0, 0) and ends at the bottom-right cell (m - 1, n - 1). The path should only follow the streets.
Notice that you are not allowed to change any street.
Return true if there is a valid path in the grid or false otherwise.
Example 1:
Input: grid = [[2,4,3],[6,5,2]]
Output: true
Explanation: As shown you can start at cell (0, 0) and visit all the cells of the grid to reach (m - 1, n - 1).Example 2:
Input: grid = [[1,2,1],[1,2,1]]
Output: false
Explanation: As shown you the street at cell (0, 0) is not connected with any street of any other cell and you will get stuck at cell (0, 0)Example 3:
Input: grid = [[1,1,2]]
Output: false
Explanation: You will get stuck at cell (0, 1) and you cannot reach cell (0, 2).
Constraints:
m == grid.lengthn == grid[i].length1 <= m, n <= 3001 <= grid[i][j] <= 6
Approaches
3 approaches with complexity analysis and trade-offs.
This approach models the grid as a graph where each cell is a node. An edge exists between two adjacent cells if their respective streets connect to each other. The problem then becomes finding if a path exists from the source node (0, 0) to the destination node (m-1, n-1). DFS is a natural way to explore this path.
Algorithm
- We model the grid as a graph where each cell is a node. An edge exists between two adjacent cells if their respective streets connect to each other. The problem then becomes finding if a path exists from the source node (0, 0) to the destination node (m-1, n-1).
- We start a traversal from the cell (0, 0).
- We use a
visitedboolean grid to keep track of cells that are already part of the current exploration path to avoid cycles and redundant computations. - The core of the approach is a recursive function, say
dfs(row, col), which explores paths from the current cell(row, col). - Algorithm Steps:
- Define the possible moves for each street type. A street
kallows moves in two directions. We can store this in a 3D array, e.g.,moves[k-1] = {{dr1, dc1}, {dr2, dc2}}. - Create a recursive function
dfs(r, c, grid, visited). - Base Case: If
(r, c)is the destination(m-1, n-1), a path is found, returntrue. - Mark the current cell
(r, c)as visited. - Get the current street type
type = grid[r][c]. - Iterate through the two possible moves
(dr, dc)for thistype. - For each move, calculate the neighbor's coordinates
(nr, nc) = (r + dr, c + dc). - Validity Checks:
- Check if
(nr, nc)is within the grid boundaries. - Check if
(nr, nc)has been visited. - Crucially, check if the street at
(nr, nc)connects back to(r, c). This means one of the allowed moves from(nr, nc)must be(-dr, -dc).
- Check if
- If the move is valid, make a recursive call
dfs(nr, nc, grid, visited). If this call returnstrue, it means a path to the destination was found, so propagatetrueup the call stack. - The initial call is
dfs(0, 0, grid, visited).
- Define the possible moves for each street type. A street
Walkthrough
class Solution { // directions[street_type - 1] = {{dr1, dc1}, {dr2, dc2}} private int[][][] directions = { {{0, -1}, {0, 1}}, // 1: left-right {{-1, 0}, {1, 0}}, // 2: up-down {{0, -1}, {1, 0}}, // 3: left-down {{0, 1}, {1, 0}}, // 4: right-down {{0, -1}, {-1, 0}}, // 5: left-up {{0, 1}, {-1, 0}} // 6: right-up }; private int m, n; private boolean[][] visited; public boolean hasValidPath(int[][] grid) { m = grid.length; n = grid[0].length; visited = new boolean[m][n]; return dfs(0, 0, grid); } private boolean dfs(int r, int c, int[][] grid) { if (r == m - 1 && c == n - 1) { return true; } visited[r][c] = true; int streetType = grid[r][c]; for (int[] dir : directions[streetType - 1]) { int nr = r + dir[0]; int nc = c + dir[1]; if (nr >= 0 && nr < m && nc >= 0 && nc < n && !visited[nr][nc]) { // Check if the next street connects back int nextStreetType = grid[nr][nc]; for (int[] backDir : directions[nextStreetType - 1]) { if (nr + backDir[0] == r && nc + backDir[1] == c) { if (dfs(nr, nc, grid)) { return true; } } } } } return false; }}Complexity
Time
O(m * n). In the worst case, we visit every cell in the grid once.
Space
O(m * n). This is for the `visited` array and the recursion stack. In the worst case, the recursion depth can be up to `m * n`.
Trade-offs
Pros
Conceptually straightforward and easy to implement.
Follows the problem's path-finding nature directly.
Cons
Can lead to a
StackOverflowErrorfor very large grids where the path is long and winding, as the recursion depth could exceed the stack limit.
This approach also treats the grid as a graph and uses BFS to find a path. BFS explores the graph layer by layer from the source. It's an iterative approach using a queue, which avoids the recursion depth limitations of DFS.
Algorithm
- BFS is an excellent algorithm for finding if a path exists in an unweighted graph. It systematically explores neighbors of the starting cell, then neighbors of those neighbors, and so on.
- We use a queue to manage the cells to visit and a
visitedgrid to prevent processing the same cell multiple times. - Algorithm Steps:
- Define the possible moves for each street type, same as in the DFS approach.
- Initialize a queue and add the starting cell
(0, 0). - Initialize a
visitedboolean grid of sizem x nand mark(0, 0)as visited. - Loop while the queue is not empty:
a. Dequeue a cell
(r, c). b. If(r, c)is the destination(m-1, n-1), returntrue. c. Get the street typetype = grid[r][c]. d. For each of the two possible moves(dr, dc)for thistype: i. Calculate the neighbor's coordinates(nr, nc) = (r + dr, c + dc). ii. Validity Checks: - Check if(nr, nc)is within grid boundaries. - Check if(nr, nc)has been visited. - Check if the street at(nr, nc)connects back to(r, c). iii. If the move is valid, enqueue(nr, nc)and mark it as visited. - If the loop finishes, it means the destination was not reachable from the source. Return
false.
Walkthrough
import java.util.LinkedList;import java.util.Queue; class Solution { public boolean hasValidPath(int[][] grid) { int m = grid.length; int n = grid[0].length; // directions[street_type - 1] = {{dr1, dc1}, {dr2, dc2}} int[][][] directions = { {{0, -1}, {0, 1}}, // 1: left-right {{-1, 0}, {1, 0}}, // 2: up-down {{0, -1}, {1, 0}}, // 3: left-down {{0, 1}, {1, 0}}, // 4: right-down {{0, -1}, {-1, 0}}, // 5: left-up {{0, 1}, {-1, 0}} // 6: right-up }; boolean[][] visited = new boolean[m][n]; Queue<int[]> queue = new LinkedList<>(); queue.offer(new int[]{0, 0}); visited[0][0] = true; while (!queue.isEmpty()) { int[] curr = queue.poll(); int r = curr[0]; int c = curr[1]; if (r == m - 1 && c == n - 1) { return true; } int streetType = grid[r][c]; for (int[] dir : directions[streetType - 1]) { int nr = r + dir[0]; int nc = c + dir[1]; if (nr >= 0 && nr < m && nc >= 0 && nc < n && !visited[nr][nc]) { int nextStreetType = grid[nr][nc]; // Check if the next street connects back to the current cell for (int[] backDir : directions[nextStreetType - 1]) { if (nr + backDir[0] == r && nc + backDir[1] == c) { visited[nr][nc] = true; queue.offer(new int[]{nr, nc}); } } } } } return false; }}Complexity
Time
O(m * n). Each cell is enqueued and dequeued at most once.
Space
O(m * n). For the `visited` array and the queue. The queue can hold up to O(m * n) cells in the worst case.
Trade-offs
Pros
Guaranteed to find a path if one exists.
Avoids stack overflow issues since it's iterative.
Generally more robust than recursive DFS for grid traversal problems.
Cons
May use more memory than DFS if the graph is wide, as the queue can grow large.
This approach reframes the problem from path-finding to a connectivity problem. The Union-Find data structure is highly optimized for determining if elements belong to the same connected component. We can iterate through the grid, uniting cells that have valid street connections. Finally, we check if the start and end cells are in the same component.
Algorithm
- The core idea is to group all connected cells into disjoint sets. If the start cell
(0, 0)and end cell(m-1, n-1)end up in the same set, a path exists between them. - Algorithm Steps:
- Create a Union-Find (DSU) data structure for
m * nelements. Each cell(r, c)is mapped to an indexr * n + c. The DSU will need aparentarray and optionally arankorsizearray for optimization (union by rank/size). - Define which street types connect to which directions. For example, a
Setfor each direction:rightPorts = {1, 4, 6},leftPorts = {1, 3, 5}, etc. - Iterate through every cell
(r, c)in the grid. - For each cell, check its neighbors in two directions (e.g., right and down) to avoid redundant checks.
- Check Right Neighbor: If
c+1is in bounds, get the street typess1 = grid[r][c]ands2 = grid[r][c+1]. Ifs1has a right port ands2has a left port, they are connected. Performunion(r*n + c, r*n + c+1). - Check Down Neighbor: If
r+1is in bounds, get street typess1 = grid[r][c]ands2 = grid[r+1][c]. Ifs1has a down port ands2has an up port, they are connected. Performunion(r*n + c, (r+1)*n + c).
- Check Right Neighbor: If
- After iterating through the entire grid, all connected cells will be in the same set.
- Check if the start cell
(0, 0)and end cell(m-1, n-1)have the same root parent:find(0) == find(m*n - 1). If they do, returntrue; otherwise, returnfalse.
- Create a Union-Find (DSU) data structure for
Walkthrough
class Solution { class DSU { int[] parent; public DSU(int n) { parent = new int[n]; for (int i = 0; i < n; i++) { parent[i] = i; } } public int find(int i) { if (parent[i] == i) { return i; } return parent[i] = find(parent[i]); } public void union(int i, int j) { int rootI = find(i); int rootJ = find(j); if (rootI != rootJ) { parent[rootI] = rootJ; } } } public boolean hasValidPath(int[][] grid) { int m = grid.length; int n = grid[0].length; // Ports: 0:up, 1:right, 2:down, 3:left int[][] ports = { {0, 0, 0, 0}, // dummy for 0 {0, 1, 0, 1}, // 1: right, left {1, 0, 1, 0}, // 2: up, down {0, 0, 1, 1}, // 3: down, left {0, 1, 1, 0}, // 4: right, down {1, 0, 0, 1}, // 5: up, left {1, 1, 0, 0} // 6: up, right }; DSU dsu = new DSU(m * n); for (int r = 0; r < m; r++) { for (int c = 0; c < n; c++) { int type = grid[r][c]; int currentIdx = r * n + c; // Check connection down if (r + 1 < m) { int downType = grid[r+1][c]; if (ports[type][2] == 1 && ports[downType][0] == 1) { dsu.union(currentIdx, (r + 1) * n + c); } } // Check connection right if (c + 1 < n) { int rightType = grid[r][c+1]; if (ports[type][1] == 1 && ports[rightType][3] == 1) { dsu.union(currentIdx, r * n + (c + 1)); } } } } return dsu.find(0) == dsu.find(m * n - 1); }}Complexity
Time
O(m * n * α(m * n)), where α is the Inverse Ackermann function. Since α grows extremely slowly, this is practically linear, i.e., O(m * n). We iterate through each cell once, and the `union` and `find` operations take nearly constant time on average.
Space
O(m * n) to store the `parent` array for the DSU data structure.
Trade-offs
Pros
Very efficient for connectivity problems. Asymptotically, it's the fastest approach.
Elegant solution that separates the connectivity logic from the path-finding goal.
Cons
It always processes the entire grid, which might be slower than BFS/DFS if the connected component of the start cell is small and doesn't contain the end cell.
Slightly more complex to implement the DSU structure compared to a standard BFS/DFS.
Solutions
Solution
class Solution {private int[] p;private int[][] grid;private int m;private int n;public boolean hasValidPath(int[][] grid) { this.grid = grid; m = grid.length; n = grid[0].length; p = new int[m * n]; for (int i = 0; i < p.length; ++i) { p[i] = i; } for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { int e = grid[i][j]; if (e == 1) { left(i, j); right(i, j); } else if (e == 2) { up(i, j); down(i, j); } else if (e == 3) { left(i, j); down(i, j); } else if (e == 4) { right(i, j); down(i, j); } else if (e == 5) { left(i, j); up(i, j); } else { right(i, j); up(i, j); } } } return find(0) == find(m * n - 1); }private int find(int x) { if (p[x] != x) { p[x] = find(p[x]); } return p[x]; }private void left(int i, int j) { if (j > 0 && (grid[i][j - 1] == 1 || grid[i][j - 1] == 4 || grid[i][j - 1] == 6)) { p[find(i * n + j)] = find(i * n + j - 1); } }private void right(int i, int j) { if (j < n - 1 && (grid[i][j + 1] == 1 || grid[i][j + 1] == 3 || grid[i][j + 1] == 5)) { p[find(i * n + j)] = find(i * n + j + 1); } }private void up(int i, int j) { if (i > 0 && (grid[i - 1][j] == 2 || grid[i - 1][j] == 3 || grid[i - 1][j] == 4)) { p[find(i * n + j)] = find((i - 1) * n + j); } }private void down(int i, int j) { if (i < m - 1 && (grid[i + 1][j] == 2 || grid[i + 1][j] == 5 || grid[i + 1][j] == 6)) { p[find(i * n + j)] = find((i + 1) * n + j); } }}class Solution {public: vector<int> p; bool hasValidPath(vector<vector<int>> &grid) { int m = grid.size(); int n = grid[0].size(); p.resize(m * n); for (int i = 0; i < p.size(); ++i) p[i] = i; auto left = [&](int i, int j) { if (j > 0 && (grid[i][j - 1] == 1 || grid[i][j - 1] == 4 || grid[i][j - 1] == 6)) { p[find(i * n + j)] = find(i * n + j - 1); } }; auto right = [&](int i, int j) { if (j < n - 1 && (grid[i][j + 1] == 1 || grid[i][j + 1] == 3 || grid[i][j + 1] == 5)) { p[find(i * n + j)] = find(i * n + j + 1); } }; auto up = [&](int i, int j) { if (i > 0 && (grid[i - 1][j] == 2 || grid[i - 1][j] == 3 || grid[i - 1][j] == 4)) { p[find(i * n + j)] = find((i - 1) * n + j); } }; auto down = [&](int i, int j) { if (i < m - 1 && (grid[i + 1][j] == 2 || grid[i + 1][j] == 5 || grid[i + 1][j] == 6)) { p[find(i * n + j)] = find((i + 1) * n + j); } }; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { int e = grid[i][j]; if (e == 1) { left(i, j); right(i, j); } else if (e == 2) { up(i, j); down(i, j); } else if (e == 3) { left(i, j); down(i, j); } else if (e == 4) { right(i, j); down(i, j); } else if (e == 5) { left(i, j); up(i, j); } else { right(i, j); up(i, j); } } } return find(0) == find(m * n - 1); } int find(int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; }};class Solution: def hasValidPath(self, grid: List[List[int]]) -> bool: m, n = len(grid), len(grid[0]) p = list(range(m * n)) def find(x): if p[x] != x: p[x] = find(p[x]) return p[x] def left(i, j): if j > 0 and grid[i][j - 1] in (1, 4, 6): p[find(i * n + j)] = find(i * n + j - 1) def right(i, j): if j < n - 1 and grid[i][j + 1] in (1, 3, 5): p[find(i * n + j)] = find(i * n + j + 1) def up(i, j): if i > 0 and grid[i - 1][j] in (2, 3, 4): p[find(i * n + j)] = find((i - 1) * n + j) def down(i, j): if i < m - 1 and grid[i + 1][j] in (2, 5, 6): p[find(i * n + j)] = find((i + 1) * n + j) for i in range(m): for j in range(n): e = grid[i][j] if e == 1: left(i, j) right(i, j) elif e == 2: up(i, j) down(i, j) elif e == 3: left(i, j) down(i, j) elif e == 4: right(i, j) down(i, j) elif e == 5: left(i, j) up(i, j) else: right(i, j) up(i, j) return find(0) == find(m * n - 1)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.