Files
codetutor/backend/data/questions/check-if-there-is-a-valid-path-in-a-grid.yaml

342 lines
14 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
title: Check if There is a Valid Path in a Grid
slug: check-if-there-is-a-valid-path-in-a-grid
difficulty: medium
leetcode_id: 1391
leetcode_url: https://leetcode.com/problems/check-if-there-is-a-valid-path-in-a-grid/
categories:
- arrays
- graphs
patterns:
- slug: bfs
is_optimal: false
- slug: dfs
is_optimal: false
- slug: matrix-traversal
is_optimal: false
- slug: union-find
is_optimal: true
function_signature: "def has_valid_path(grid: list[list[int]]) -> bool:"
test_cases:
visible:
- input: { grid: [[2, 4, 3], [6, 5, 2]] }
expected: true
- input: { grid: [[1, 2, 1], [1, 2, 1]] }
expected: false
- input: { grid: [[1, 1, 2]] }
expected: false
hidden:
- input: { grid: [[1]] }
expected: true
- input: { grid: [[2]] }
expected: true
- input: { grid: [[4, 1], [6, 1]] }
expected: true
- input: { grid: [[1, 1, 1, 1]] }
expected: true
- input: { grid: [[2], [2], [2]] }
expected: true
- input: { grid: [[4, 3], [6, 5]] }
expected: false
description: |
You are given an `m x n` `grid`. Each cell of `grid` represents a street. The street of `grid[i][j]` can be:
- `1` which means a street connecting the **left** cell and the **right** cell.
- `2` which means a street connecting the **upper** cell and the **lower** cell.
- `3` which means a street connecting the **left** cell and the **lower** cell.
- `4` which means a street connecting the **right** cell and the **lower** cell.
- `5` which means a street connecting the **left** cell and the **upper** cell.
- `6` which 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*.
constraints: |
- `m == grid.length`
- `n == grid[i].length`
- `1 <= m, n <= 300`
- `1 <= grid[i][j] <= 6`
examples:
- input: "grid = [[2,4,3],[6,5,2]]"
output: "true"
explanation: "You can start at cell (0, 0) and visit all the cells of the grid to reach (m - 1, n - 1) by following the connected streets."
- input: "grid = [[1,2,1],[1,2,1]]"
output: "false"
explanation: "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)."
- input: "grid = [[1,1,2]]"
output: "false"
explanation: "You will get stuck at cell (0, 1) and you cannot reach cell (0, 2)."
explanation:
intuition: |
Imagine each cell as a **pipe segment** in a plumbing system. Each pipe type connects two specific sides of the cell — some go left-right, some go up-down, and others bend in various L-shapes.
For water (or a path) to flow from one cell to another, **both pipes must connect at their shared edge**. If cell A has a pipe opening on its right side, cell B (to the right of A) must have a pipe opening on its left side for them to be connected.
The key insight is that this is a **bidirectional connectivity problem**. When moving from cell A to cell B:
1. Cell A must have an opening toward B (the direction we want to go)
2. Cell B must have an opening back toward A (the direction we came from)
Think of it like puzzle pieces — the edges must match up. A pipe that opens right (`→`) can only connect to a neighbour that opens left (`←`).
We can solve this using either BFS/DFS (traversing connected cells) or Union-Find (grouping connected cells), checking at each step that both the source and destination cells have compatible openings.
approach: |
We solve this using **BFS with directional validation**:
**Step 1: Define pipe directions**
- Create a mapping for each pipe type (1-6) to the directions it connects
- Directions: `0` = up, `1` = right, `2` = down, `3` = left
- Pipe 1: `{1, 3}` (left-right), Pipe 2: `{0, 2}` (up-down), etc.
&nbsp;
**Step 2: Define opposite directions**
- When moving right (direction 1), the target must accept from left (direction 3)
- Opposites: `0 ↔ 2` (up ↔ down), `1 ↔ 3` (right ↔ left)
&nbsp;
**Step 3: BFS traversal**
- Start from `(0, 0)` and add it to the queue
- For each cell, check all directions its pipe connects to
- For each direction, calculate the neighbour cell coordinates
- Verify the neighbour is in bounds and the neighbour's pipe connects back
- If valid and unvisited, add to queue and mark as visited
&nbsp;
**Step 4: Check destination**
- If we reach `(m-1, n-1)`, return `true`
- If the queue empties without reaching it, return `false`
common_pitfalls:
- title: Forgetting Bidirectional Validation
description: |
A common mistake is only checking if the current cell can move in a direction, without verifying the target cell accepts connections from that direction.
For example, if cell `(0, 0)` has pipe type 1 (left-right), you might try moving right to `(0, 1)`. But if `(0, 1)` has pipe type 2 (up-down), there's no connection — pipe 2 doesn't have a left opening.
Always check **both sides** of the connection.
wrong_approach: "Only checking if current cell has an opening in the movement direction"
correct_approach: "Verify both current cell's exit direction AND neighbour's entry direction match"
- title: Incorrect Direction Mapping
description: |
The six pipe types have specific connection patterns that must be encoded correctly:
- Type 1: left ↔ right (horizontal)
- Type 2: up ↔ down (vertical)
- Type 3: left ↔ down (L-bend)
- Type 4: right ↔ down (L-bend)
- Type 5: left ↔ up (L-bend)
- Type 6: right ↔ up (L-bend)
Getting even one mapping wrong will cause incorrect path detection.
wrong_approach: "Guessing pipe directions without careful mapping"
correct_approach: "Create explicit direction sets for each pipe type based on the problem description"
- title: Not Handling Single Cell Grid
description: |
When the grid is `1x1`, the start and end are the same cell. The path is trivially valid regardless of the pipe type.
Make sure to handle this edge case by checking if `(0, 0) == (m-1, n-1)` at the start.
wrong_approach: "Assuming there's always movement required"
correct_approach: "Return true immediately if start equals destination"
key_takeaways:
- "**Bidirectional validation**: In connectivity problems with directional constraints, verify both source and destination agree on the connection"
- "**Direction encoding**: Use consistent direction indices (0=up, 1=right, 2=down, 3=left) and precompute direction deltas `[(-1,0), (0,1), (1,0), (0,-1)]`"
- "**BFS vs Union-Find**: Both work here — BFS explores reachable cells, Union-Find groups connected cells. BFS is more intuitive; Union-Find can be faster for multiple queries"
- "**Pattern recognition**: This is a specialised graph traversal where edges have compatibility constraints, similar to pipe puzzles or tile-matching games"
time_complexity: "O(m × n). Each cell is visited at most once during BFS traversal, and we perform constant-time direction checks per cell."
space_complexity: "O(m × n). We store a visited set that can grow up to the size of the grid, plus the BFS queue which in the worst case holds O(min(m, n)) elements."
solutions:
- approach_name: BFS with Direction Validation
is_optimal: true
code: |
from collections import deque
def has_valid_path(grid: list[list[int]]) -> bool:
m, n = len(grid), len(grid[0])
# Single cell grid is always valid
if m == 1 and n == 1:
return True
# Direction indices: 0=up, 1=right, 2=down, 3=left
# Each pipe type maps to the directions it connects
pipe_dirs = {
1: {1, 3}, # left-right (horizontal)
2: {0, 2}, # up-down (vertical)
3: {2, 3}, # left-down
4: {1, 2}, # right-down
5: {0, 3}, # left-up
6: {0, 1}, # right-up
}
# Direction deltas: [up, right, down, left]
deltas = [(-1, 0), (0, 1), (1, 0), (0, -1)]
# Opposite directions for bidirectional check
opposite = {0: 2, 1: 3, 2: 0, 3: 1}
visited = {(0, 0)}
queue = deque([(0, 0)])
while queue:
r, c = queue.popleft()
# Check if we've reached the destination
if r == m - 1 and c == n - 1:
return True
# Get directions current pipe connects to
current_pipe = grid[r][c]
for direction in pipe_dirs[current_pipe]:
dr, dc = deltas[direction]
nr, nc = r + dr, c + dc
# Check bounds
if 0 <= nr < m and 0 <= nc < n and (nr, nc) not in visited:
# Check if neighbour pipe connects back
neighbour_pipe = grid[nr][nc]
if opposite[direction] in pipe_dirs[neighbour_pipe]:
visited.add((nr, nc))
queue.append((nr, nc))
return False
explanation: |
**Time Complexity:** O(m × n) — Each cell visited once.
**Space Complexity:** O(m × n) — Visited set and queue storage.
We use BFS to explore all reachable cells from the start. For each cell, we check which directions its pipe type connects to, then verify the neighbour in that direction has a compatible pipe (one that connects back). This bidirectional check ensures we only traverse valid pipe connections.
- approach_name: Union-Find
is_optimal: true
code: |
def has_valid_path(grid: list[list[int]]) -> bool:
m, n = len(grid), len(grid[0])
# Union-Find with path compression
parent = list(range(m * n))
def find(x: int) -> int:
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
def union(x: int, y: int) -> None:
px, py = find(x), find(y)
if px != py:
parent[px] = py
# Convert 2D to 1D index
def idx(r: int, c: int) -> int:
return r * n + c
# Pipe directions: which sides each pipe connects
# Directions: 0=up, 1=right, 2=down, 3=left
pipe_dirs = {
1: {1, 3}, # left-right
2: {0, 2}, # up-down
3: {2, 3}, # left-down
4: {1, 2}, # right-down
5: {0, 3}, # left-up
6: {0, 1}, # right-up
}
# Only check right and down to avoid duplicate unions
deltas = [(2, 1, 0), (1, 0, 1)] # (dir, dr, dc)
opposite = {0: 2, 1: 3, 2: 0, 3: 1}
for r in range(m):
for c in range(n):
current_pipe = grid[r][c]
# Check right neighbour
if c + 1 < n:
if 1 in pipe_dirs[current_pipe]: # current opens right
neighbour_pipe = grid[r][c + 1]
if 3 in pipe_dirs[neighbour_pipe]: # neighbour opens left
union(idx(r, c), idx(r, c + 1))
# Check down neighbour
if r + 1 < m:
if 2 in pipe_dirs[current_pipe]: # current opens down
neighbour_pipe = grid[r + 1][c]
if 0 in pipe_dirs[neighbour_pipe]: # neighbour opens up
union(idx(r, c), idx(r + 1, c))
# Check if start and end are connected
return find(idx(0, 0)) == find(idx(m - 1, n - 1))
explanation: |
**Time Complexity:** O(m × n × α(m × n)) ≈ O(m × n) — Union-Find operations with path compression are nearly constant time.
**Space Complexity:** O(m × n) — Parent array for Union-Find.
We iterate through each cell and union it with valid neighbours (right and down only, to avoid duplicate work). Two cells are unioned only if both pipes have compatible openings. Finally, we check if the start and end cells belong to the same connected component.
- approach_name: DFS with Direction Validation
is_optimal: true
code: |
def has_valid_path(grid: list[list[int]]) -> bool:
m, n = len(grid), len(grid[0])
# Pipe directions mapping
pipe_dirs = {
1: {1, 3}, # left-right
2: {0, 2}, # up-down
3: {2, 3}, # left-down
4: {1, 2}, # right-down
5: {0, 3}, # left-up
6: {0, 1}, # right-up
}
deltas = [(-1, 0), (0, 1), (1, 0), (0, -1)]
opposite = {0: 2, 1: 3, 2: 0, 3: 1}
visited = set()
def dfs(r: int, c: int) -> bool:
# Reached destination
if r == m - 1 and c == n - 1:
return True
visited.add((r, c))
current_pipe = grid[r][c]
for direction in pipe_dirs[current_pipe]:
dr, dc = deltas[direction]
nr, nc = r + dr, c + dc
if 0 <= nr < m and 0 <= nc < n and (nr, nc) not in visited:
neighbour_pipe = grid[nr][nc]
# Check bidirectional connection
if opposite[direction] in pipe_dirs[neighbour_pipe]:
if dfs(nr, nc):
return True
return False
return dfs(0, 0)
explanation: |
**Time Complexity:** O(m × n) — Each cell visited at most once.
**Space Complexity:** O(m × n) — Recursion stack and visited set.
DFS explores paths depth-first, backtracking when hitting dead ends. The logic mirrors BFS: for each cell, we try all valid directions, verify the neighbour accepts the connection, and recursively explore. This approach is conceptually simpler but may hit Python's recursion limit on very large grids.