1496. Path Crossing
Problem Description
You are given a string path
consisting of characters 'N'
, 'S'
, 'E'
, and 'W'
, where each character represents a directional movement:
'N'
means move one unit north (up)'S'
means move one unit south (down)'E'
means move one unit east (right)'W'
means move one unit west (left)
You start at the origin point (0, 0)
on a 2D coordinate plane and follow the movements specified by the characters in path
in order.
The task is to determine if you ever visit the same coordinate twice during your journey. Return true
if at any point you step on a location you've already visited (including potentially returning to the origin), and false
if you never revisit any coordinate.
For example:
- If
path = "NES"
, you move north to(0, 1)
, then east to(1, 1)
, then south to(1, 0)
. Since no coordinate is visited twice, the answer isfalse
. - If
path = "NESWW"
, you move north to(0, 1)
, east to(1, 1)
, south to(1, 0)
, west to(0, 0)
, and west again to(-1, 0)
. Since you return to the origin(0, 0)
on the fourth move, the answer istrue
.
Intuition
The key insight is that we need to track every position we've visited as we walk through the path. If we ever arrive at a position we've been to before, we know the path crosses itself.
Think of it like leaving footprints as you walk. Each time you take a step, you check if there's already a footprint at your new location. If there is, you've crossed your own path.
To implement this, we can:
- Keep track of our current position using coordinates
(i, j)
wherei
represents the vertical position andj
represents the horizontal position - Store all visited positions in a set for fast lookup - checking if a position exists in a set is an
O(1)
operation - Start by adding the origin
(0, 0)
to our visited set since that's where we begin - For each movement in the path, update our coordinates accordingly and check if the new position already exists in our visited set
The coordinate updates follow a simple pattern:
- Moving north decreases the vertical coordinate by 1:
i -= 1
- Moving south increases the vertical coordinate by 1:
i += 1
- Moving east increases the horizontal coordinate by 1:
j += 1
- Moving west decreases the horizontal coordinate by 1:
j -= 1
If at any point our new position (i, j)
is already in the visited set, we immediately return true
because we've crossed our path. If we complete the entire journey without revisiting any position, we return false
.
This approach is efficient because we only need to traverse the path once, and each position check takes constant time using the set data structure.
Solution Approach
The implementation uses a hash set to track visited coordinates and simulates the path traversal step by step.
Data Structure:
- Use a set
vis
to store visited coordinates as tuples(i, j)
- Initialize
vis
with the starting position(0, 0)
Algorithm Steps:
-
Initialize position and visited set:
i = j = 0 # Starting at origin vis = {(0, 0)} # Mark origin as visited
-
Process each movement in the path:
- Iterate through each character
c
in the path string - Use a match-case statement (Python 3.10+) to update coordinates based on direction:
match c: case 'N': i -= 1 # Move north (up) case 'S': i += 1 # Move south (down) case 'E': j += 1 # Move east (right) case 'W': j -= 1 # Move west (left)
- Iterate through each character
-
Check for path crossing:
- After each move, check if the new position
(i, j)
already exists invis
- If it exists, we've crossed our path, so return
True
- Otherwise, add the new position to
vis
:if (i, j) in vis: return True vis.add((i, j))
- After each move, check if the new position
-
Return result:
- If we complete the entire path without finding any duplicate positions, return
False
- If we complete the entire path without finding any duplicate positions, return
Time Complexity: O(n)
where n
is the length of the path string. We visit each character once, and set operations (lookup and insertion) are O(1)
on average.
Space Complexity: O(n)
in the worst case, as we might need to store up to n + 1
unique positions (including the origin) if the path never crosses itself.
The solution elegantly handles the problem by maintaining a single set of visited positions and checking for duplicates in real-time as we traverse the path, eliminating the need for any complex geometric calculations.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the path "NESW"
step by step to see if we cross our own path:
Initial Setup:
- Starting position:
(0, 0)
- Visited set:
{(0, 0)}
Step 1: Process 'N' (North)
- Current position:
(0, 0)
- Move north:
i = 0 - 1 = -1
,j = 0
- New position:
(-1, 0)
- Check if
(-1, 0)
is in visited set: No - Add
(-1, 0)
to visited set - Visited set:
{(0, 0), (-1, 0)}
Step 2: Process 'E' (East)
- Current position:
(-1, 0)
- Move east:
i = -1
,j = 0 + 1 = 1
- New position:
(-1, 1)
- Check if
(-1, 1)
is in visited set: No - Add
(-1, 1)
to visited set - Visited set:
{(0, 0), (-1, 0), (-1, 1)}
Step 3: Process 'S' (South)
- Current position:
(-1, 1)
- Move south:
i = -1 + 1 = 0
,j = 1
- New position:
(0, 1)
- Check if
(0, 1)
is in visited set: No - Add
(0, 1)
to visited set - Visited set:
{(0, 0), (-1, 0), (-1, 1), (0, 1)}
Step 4: Process 'W' (West)
- Current position:
(0, 1)
- Move west:
i = 0
,j = 1 - 1 = 0
- New position:
(0, 0)
- Check if
(0, 0)
is in visited set: Yes! - We've returned to the origin, so we've crossed our path
- Return True
The path forms a square, and we end up back at the starting position (0, 0)
, which means we've crossed our own path.
Solution Implementation
1class Solution:
2 def isPathCrossing(self, path: str) -> bool:
3 """
4 Determines if a path crosses itself by visiting the same coordinate twice.
5
6 Args:
7 path: A string containing movement directions ('N', 'S', 'E', 'W')
8
9 Returns:
10 True if the path crosses itself, False otherwise
11 """
12 # Initialize starting position at origin (0, 0)
13 row = 0
14 col = 0
15
16 # Set to track all visited coordinates
17 visited = {(0, 0)}
18
19 # Process each direction in the path
20 for direction in path:
21 # Update position based on direction
22 if direction == 'N':
23 row -= 1 # Move north (up)
24 elif direction == 'S':
25 row += 1 # Move south (down)
26 elif direction == 'E':
27 col += 1 # Move east (right)
28 elif direction == 'W':
29 col -= 1 # Move west (left)
30
31 # Check if we've visited this coordinate before
32 if (row, col) in visited:
33 return True # Path crosses itself
34
35 # Add current position to visited set
36 visited.add((row, col))
37
38 # No crossing detected
39 return False
40
1class Solution {
2 public boolean isPathCrossing(String path) {
3 // Current position coordinates
4 int x = 0;
5 int y = 0;
6
7 // Set to store visited positions
8 // Using hash encoding to represent 2D coordinates as a single integer
9 Set<Integer> visitedPositions = new HashSet<>();
10
11 // Add starting position (0, 0) to visited set
12 visitedPositions.add(0);
13
14 // Iterate through each character in the path
15 for (int i = 0; i < path.length(); i++) {
16 char direction = path.charAt(i);
17
18 // Update position based on direction
19 switch (direction) {
20 case 'N': // North: move up (decrease x coordinate)
21 x--;
22 break;
23 case 'S': // South: move down (increase x coordinate)
24 x++;
25 break;
26 case 'E': // East: move right (increase y coordinate)
27 y++;
28 break;
29 case 'W': // West: move left (decrease y coordinate)
30 y--;
31 break;
32 }
33
34 // Encode the 2D position into a single integer
35 // Multiplying x by 20000 ensures no collision for valid coordinates
36 // (assumes coordinates are within reasonable bounds)
37 int encodedPosition = x * 20000 + y;
38
39 // Check if this position has been visited before
40 // add() returns false if the element already exists
41 if (!visitedPositions.add(encodedPosition)) {
42 return true; // Path crosses itself
43 }
44 }
45
46 // No crossing detected
47 return false;
48 }
49}
50
1class Solution {
2public:
3 bool isPathCrossing(string path) {
4 // Initialize starting coordinates at origin (0, 0)
5 int x = 0, y = 0;
6
7 // Use unordered_set to store visited coordinates
8 // Hash the starting position (0, 0) as 0
9 unordered_set<int> visitedPositions{{0}};
10
11 // Traverse each direction in the path
12 for (char& direction : path) {
13 // Update coordinates based on direction
14 if (direction == 'N') {
15 y++; // Move north (up)
16 } else if (direction == 'S') {
17 y--; // Move south (down)
18 } else if (direction == 'E') {
19 x++; // Move east (right)
20 } else { // direction == 'W'
21 x--; // Move west (left)
22 }
23
24 // Create unique hash for current position
25 // Using 20000 as offset to handle negative coordinates
26 // This ensures unique hash for coordinates in range [-10000, 10000]
27 int hashedPosition = x * 20000 + y;
28
29 // Check if current position was visited before
30 if (visitedPositions.count(hashedPosition)) {
31 return true; // Path crosses itself
32 }
33
34 // Add current position to visited set
35 visitedPositions.insert(hashedPosition);
36 }
37
38 // No crossing detected
39 return false;
40 }
41};
42
1/**
2 * Determines if a path crosses itself by visiting the same coordinate twice
3 * @param path - A string containing movement directions ('N', 'S', 'E', 'W')
4 * @returns true if the path crosses itself, false otherwise
5 */
6function isPathCrossing(path: string): boolean {
7 // Initialize starting position at origin (0, 0)
8 let currentRow: number = 0;
9 let currentCol: number = 0;
10
11 // Set to store visited coordinates as encoded integers
12 const visitedPositions: Set<number> = new Set<number>();
13
14 // Add the starting position to visited set
15 // Encoding: row * 20000 + col (20000 is used as offset to handle negative coordinates)
16 visitedPositions.add(0);
17
18 // Process each direction in the path
19 for (const direction of path) {
20 // Update position based on direction
21 if (direction === 'N') {
22 currentRow--; // North: move up (decrease row)
23 } else if (direction === 'S') {
24 currentRow++; // South: move down (increase row)
25 } else if (direction === 'E') {
26 currentCol++; // East: move right (increase column)
27 } else if (direction === 'W') {
28 currentCol--; // West: move left (decrease column)
29 }
30
31 // Encode current position as a single integer
32 // Using 20000 as multiplier ensures unique encoding for reasonable coordinate ranges
33 const encodedPosition: number = currentRow * 20000 + currentCol;
34
35 // Check if we've visited this position before
36 if (visitedPositions.has(encodedPosition)) {
37 return true; // Path crosses itself
38 }
39
40 // Mark current position as visited
41 visitedPositions.add(encodedPosition);
42 }
43
44 // No crossing detected
45 return false;
46}
47
Time and Space Complexity
Time Complexity: O(n)
where n
is the length of the path string.
- We iterate through each character in the path string exactly once
- For each character, we perform constant time operations:
- Pattern matching with the match-case statement:
O(1)
- Checking if a coordinate exists in the set:
O(1)
average case for hash set lookup - Adding a coordinate to the set:
O(1)
average case for hash set insertion
- Pattern matching with the match-case statement:
- Total time complexity:
O(n) * O(1) = O(n)
Space Complexity: O(n)
where n
is the length of the path string.
- The
vis
set stores visited coordinates as tuples - In the worst case, the path never crosses itself, meaning we visit
n + 1
unique positions (including the starting position(0, 0)
) - Each tuple
(i, j)
takes constant space - Total space complexity:
O(n + 1) = O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Initialize the Origin in the Visited Set
One of the most common mistakes is forgetting to add the starting position (0, 0)
to the visited set before processing the path. This leads to incorrect results when the path returns to the origin.
Incorrect Implementation:
def isPathCrossing(self, path: str) -> bool:
row = col = 0
visited = set() # ❌ Empty set - origin not included!
for direction in path:
if direction == 'N':
row -= 1
elif direction == 'S':
row += 1
elif direction == 'E':
col += 1
elif direction == 'W':
col -= 1
if (row, col) in visited:
return True
visited.add((row, col))
return False
Problem: If the path is "NESW"
(which forms a square returning to origin), this incorrect code would return False
instead of True
because (0, 0)
was never added to the visited set initially.
Correct Solution:
visited = {(0, 0)} # ✅ Include origin from the start
2. Checking for Crossing Before Moving
Another pitfall is checking if the current position has been visited before actually moving to the new position.
Incorrect Implementation:
def isPathCrossing(self, path: str) -> bool:
row = col = 0
visited = {(0, 0)}
for direction in path:
# ❌ Checking before moving
if (row, col) in visited:
return True
if direction == 'N':
row -= 1
elif direction == 'S':
row += 1
elif direction == 'E':
col += 1
elif direction == 'W':
col -= 1
visited.add((row, col))
return False
Problem: This always returns True
on the first iteration because (0, 0)
is already in visited, even though we haven't actually moved yet.
Correct Solution:
for direction in path: # ✅ Move first if direction == 'N': row -= 1 # ... other directions # ✅ Then check the new position if (row, col) in visited: return True visited.add((row, col))
3. Using Mutable Objects as Set Elements
Using lists instead of tuples for coordinates will cause a runtime error since lists are mutable and cannot be added to sets.
Incorrect Implementation:
visited = {[0, 0]} # ❌ TypeError: unhashable type: 'list' # or later in the code: visited.add([row, col]) # ❌ TypeError
Correct Solution:
visited = {(0, 0)} # ✅ Use tuples (immutable) visited.add((row, col)) # ✅ Tuples are hashable
4. Incorrect Coordinate System Interpretation
Mixing up the coordinate system directions can lead to wrong results. Be consistent with your interpretation:
Potential Confusion:
# Some might interpret coordinates differently: if direction == 'N': row += 1 # ❌ If treating north as positive y # vs if direction == 'N': row -= 1 # ✅ If treating north as negative row index (array-style)
Solution: Pick one coordinate system and stick with it consistently. The implementation should match the problem's expected behavior. Generally in 2D arrays, moving "up" (north) decreases the row index.
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!