Escape a Large Maze
HardPrompt
There is a 1 million by 1 million grid on an XY-plane, and the coordinates of each grid square are (x, y).
We start at the source = [sx, sy] square and want to reach the target = [tx, ty] square. There is also an array of blocked squares, where each blocked[i] = [xi, yi] represents a blocked square with coordinates (xi, yi).
Each move, we can walk one square north, east, south, or west if the square is not in the array of blocked squares. We are also not allowed to walk outside of the grid.
Return true if and only if it is possible to reach the target square from the source square through a sequence of valid moves.
Example 1:
Input: blocked = [[0,1],[1,0]], source = [0,0], target = [0,2]
Output: false
Explanation: The target square is inaccessible starting from the source square because we cannot move.
We cannot move north or east because those squares are blocked.
We cannot move south or west because we cannot go outside of the grid.Example 2:
Input: blocked = [], source = [0,0], target = [999999,999999]
Output: true
Explanation: Because there are no blocked cells, it is possible to reach the target square.
Constraints:
0 <= blocked.length <= 200blocked[i].length == 20 <= xi, yi < 106source.length == target.length == 20 <= sx, sy, tx, ty < 106source != target- It is guaranteed that
sourceandtargetare not blocked.
Approaches
2 approaches with complexity analysis and trade-offs.
A straightforward but impractical approach is to model the entire 10^6 x 10^6 grid as a graph and perform a standard traversal algorithm like Breadth-First Search (BFS) or Depth-First Search (DFS). We would start at the source and explore adjacent cells, keeping track of visited cells to avoid cycles, until we either find the target or exhaust all reachable cells.
Algorithm
- Treat the 10^6 x 10^6 grid as a graph.
- Start a Breadth-First Search (BFS) or Depth-First Search (DFS) from the
sourcecoordinate. - Use a queue (for BFS) or stack (for DFS) to manage coordinates to visit.
- Use a large hash set to keep track of
visitedcoordinates to avoid cycles and redundant computations. - In each step, explore the four neighbors (north, east, south, west).
- If a neighbor is within the grid boundaries, not blocked, and not visited, add it to the queue/stack and the visited set.
- Continue the search until the
targetcoordinate is found (returntrue) or the queue/stack becomes empty (returnfalse).
Walkthrough
This method directly translates the problem into a graph traversal problem without considering the specific constraints. The grid cells are the nodes, and valid moves between adjacent cells are the edges. A standard BFS would guarantee finding the shortest path if one exists, but we only need to determine existence. However, the sheer size of the grid makes this approach infeasible. The number of cells is 10^12, so any algorithm that attempts to visit a significant fraction of the grid or store their visited status will fail due to time and memory constraints.
Complexity
Time
O(N*M) in the worst case, where N and M are the grid dimensions. This is far too slow for the given constraints.
Space
O(N*M), where N and M are the dimensions of the grid (10^6 x 10^6). This is required for the `visited` set and is prohibitively large.
Trade-offs
Pros
Conceptually simple and easy to understand.
Cons
Memory Limit Exceeded: The
visitedset could potentially store up to 10^12 coordinates, which is far beyond the memory capacity of any typical competitive programming environment.Time Limit Exceeded: The search space is enormous. In an open grid, the search would explore a vast number of cells before reaching a distant target, leading to a timeout.
This approach leverages the crucial constraint that the number of blocked cells is very small (<= 200). These few blocks can only cordon off a finite, limited area. If the source is not trapped within such an area, it can access the vast, open parts of the grid. The same applies to the target. If both the source and target can reach this "open space," they can surely reach each other.
We can quantify the maximum possible area that can be enclosed. With B blocks, the largest area you can enclose is less than B*B / 2. We use this as a threshold. We perform a Breadth-First Search (BFS) from the source. If the search explores more cells than this threshold, we know the source isn't trapped. We do the same for the target. If both are not trapped, a path exists. This check also handles the case where the source and target are close and in the same enclosed region, as the BFS from source would find target directly.
Algorithm
- Key Insight: A small number of blocks (
B<= 200) can only enclose a relatively small area. The maximum area thatBblocks can trap isB * (B - 1) / 2. ForB=200, this is19900cells. - Strategy: If a point (source or target) can reach more cells than this maximum trappable area, it must be in an open region and can be considered to have "escaped". If both source and target can escape, they can reach each other through the vast open grid.
- Implementation: We run two separate, bounded BFS searches:
a. One from
sourcetotarget. b. One fromtargettosource. - Bounded BFS Logic (
bfs(start, end)): a. Store blocked cells in aHashSetfor O(1) lookups. b. Initialize a queue for BFS and avisitedset. c. Set alimitfor the search area, e.g.,limit = blocked.length * (blocked.length - 1) / 2. d. Begin BFS fromstart. e. In the BFS loop, ifvisited.size()exceeds thelimit, it meansstarthas escaped. Returntrue. f. If theendpoint is found, a path exists. Returntrue. g. If the queue becomes empty before either of the above conditions is met, it meansstartis trapped in a small area that doesn't containend. Returnfalse. - Final Result: The function returns
trueonly if bothbfs(source, target)ANDbfs(target, source)returntrue. This covers all cases: both are in a large open area, or both are in the same small enclosed area.
Walkthrough
The algorithm is implemented by creating a helper function, bfs(start, end, blockedSet, limit), which performs a bounded search. This function returns true if it either reaches end or if the number of visited cells surpasses the limit. It returns false if the reachable area from start is exhausted and is smaller than the limit and does not contain end.
The main function then calls this helper twice: bfs(source, target, ...) and bfs(target, source, ...). Both must be true for a path to be guaranteed. This correctly handles all scenarios:
- Source and Target in the same small region: The first call finds the target and returns
true. The second call finds the source and returnstrue. Result:true. - Source trapped, Target in open space: The first call returns
false. Result:false. - Source and Target in different trapped regions: The first call returns
false. Result:false. - Source and Target in open space: Both calls return
truebecause their search areas will exceed the limit. Result:true.
To handle the large coordinates efficiently, we convert each (x, y) pair into a single long value (key = x * 10^6 + y) for use in HashSets.
import java.util.HashSet;import java.util.LinkedList;import java.util.Queue;import java.util.Set; class Solution { private static final long GRID_SIZE = 1_000_000L; private int[][] dirs = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; public boolean isEscapePossible(int[][] blocked, int[] source, int[] target) { Set<Long> blockedSet = new HashSet<>(); for (int[] block : blocked) { blockedSet.add((long)block[0] * GRID_SIZE + block[1]); } // With B blocks, the maximum area they can enclose is B*(B-1)/2. // If we visit more than this number of cells, we have escaped the enclosure. int limit = blocked.length * (blocked.length - 1) / 2; return bfs(source, target, blockedSet, limit) && bfs(target, source, blockedSet, limit); } private boolean bfs(int[] start, int[] end, Set<Long> blockedSet, int limit) { Queue<int[]> queue = new LinkedList<>(); Set<Long> visited = new HashSet<>(); long startKey = (long)start[0] * GRID_SIZE + start[1]; long endKey = (long)end[0] * GRID_SIZE + end[1]; queue.offer(start); visited.add(startKey); while (!queue.isEmpty()) { // If the number of visited cells exceeds the limit, we have escaped. if (visited.size() > limit) { return true; } int[] current = queue.poll(); if ((long)current[0] * GRID_SIZE + current[1] == endKey) { return true; } for (int[] dir : dirs) { int nextX = current[0] + dir[0]; int nextY = current[1] + dir[1]; long nextKey = (long)nextX * GRID_SIZE + nextY; if (nextX >= 0 && nextX < GRID_SIZE && nextY >= 0 && nextY < GRID_SIZE && !blockedSet.contains(nextKey) && visited.add(nextKey)) { // .add() returns true if the element was new queue.offer(new int[]{nextX, nextY}); } } } // If the queue is empty, we are trapped in a small area. return false; }}Complexity
Time
O(B^2), where B is the number of blocked cells. We perform two BFS runs, and each search explores at most `O(B^2)` cells. Creating the initial `blockedSet` takes O(B).
Space
O(B^2), where B is the number of blocked cells. The `visited` set and the BFS `queue` will store at most `O(B^2)` elements in the worst case for each of the two BFS runs.
Trade-offs
Pros
Highly Efficient: Avoids exploring the entire grid by limiting the search space based on the number of blocks.
Correctness: The logic correctly handles all cases, including when source/target are trapped or in open space.
Optimal Complexity: The time and space complexity are polynomial in the number of blocks, not the grid size, making it feasible.
Cons
The logic of needing two separate checks (
sourcetotargetandtargettosource) might not be immediately intuitive.Requires careful handling of large coordinates to use as keys in hash sets (e.g., converting
(x, y)to a singlelong).
Solutions
Solution
class Solution {private int[][] dirs = new int[][]{{1, 0}, {-1, 0}, {0, 1}, {0, -1}};private static final int N = (int)1 e6;private Set<Integer> blocked;public boolean isEscapePossible(int[][] blocked, int[] source, int[] target) { this.blocked = new HashSet<>(); for (int[] b : blocked) { this.blocked.add(b[0] * N + b[1]); } return dfs(source, target, new HashSet<>()) && dfs(target, source, new HashSet<>()); }private boolean dfs(int[] source, int[] target, Set<Integer> seen) { int sx = source[0], sy = source[1]; int tx = target[0], ty = target[1]; if (sx < 0 || sx >= N || sy < 0 || sy >= N || tx < 0 || tx >= N || ty < 0 || ty >= N || blocked.contains(sx * N + sy) || seen.contains(sx * N + sy)) { return false; } seen.add(sx * N + sy); if (seen.size() > 20000 || (sx == target[0] && sy == target[1])) { return true; } for (int[] dir : dirs) { if (dfs(new int[]{sx + dir[0], sy + dir[1]}, target, seen)) { return true; } } return false; }}typedef unsigned long long ULL ; class Solution { public: vector < vector < int >> dirs = { { 0 , 1 }, { 0 , - 1 }, { 1 , 0 }, { - 1 , 0 } }; unordered_set < ULL > blocked ; int N = 1e6 ; bool isEscapePossible ( vector < vector < int >>& blocked , vector < int >& source , vector < int >& target ) { this -> blocked . clear (); for ( auto & b : blocked ) this -> blocked . insert (( ULL ) b [ 0 ] * N + b [ 1 ]); unordered_set < ULL > s1 ; unordered_set < ULL > s2 ; return dfs ( source , target , s1 ) && dfs ( target , source , s2 ); } bool dfs ( vector < int >& source , vector < int >& target , unordered_set < ULL >& seen ) { int sx = source [ 0 ], sy = source [ 1 ]; int tx = target [ 0 ], ty = target [ 1 ]; if ( sx < 0 || sx >= N || sy < 0 || sy >= N || tx < 0 || tx >= N || ty < 0 || ty >= N || blocked . count (( ULL ) sx * N + sy ) || seen . count (( ULL ) sx * N + sy )) return 0 ; seen . insert (( ULL ) sx * N + sy ); if ( seen . size () > 20000 || ( sx == target [ 0 ] && sy == target [ 1 ])) return 1 ; for ( auto & dir : dirs ) { vector < int > next = { sx + dir [ 0 ], sy + dir [ 1 ]}; if ( dfs ( next , target , seen )) return 1 ; } return 0 ; } };class Solution: def isEscapePossible(self, blocked: List[List[int]], source: List[int], target: List[int]) -> bool: def dfs(source, target, seen): x, y = source if (not (0 <= x < 10 ** 6 and 0 <= y < 10 ** 6) or (x, y) in blocked or (x, y) in seen): return False seen . add((x, y)) if len(seen) > 20000 or source == target: return True for a, b in [[0, - 1], [0, 1], [1, 0], [- 1, 0]]: next = [x + a, y + b] if dfs(next, target, seen): return True return False blocked = set((x, y) for x, y in blocked) return dfs(source, target, set()) and dfs(target, source, set())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.