LeetCode 2069 - Walking Robot Simulation II
The problem describes a simulation of a robot moving on a rectangular grid defined by width x height. The robot starts at the bottom-left corner (0, 0) facing East and moves in discrete steps.
Difficulty: 🟡 Medium
Topics: Design, Simulation
Solution
Problem Understanding
The problem describes a simulation of a robot moving on a rectangular grid defined by width x height. The robot starts at the bottom-left corner (0, 0) facing East and moves in discrete steps. Each step moves the robot forward in its current direction unless the next cell is out of bounds, in which case the robot rotates 90 degrees counterclockwise and retries that step. The goal is to implement a class that can efficiently handle a potentially large number of steps while providing the robot’s current position and facing direction.
The input consists of the grid dimensions and a sequence of step instructions, where each instruction specifies the number of steps to move. The output consists of the robot’s position as [x, y] and its facing direction as a string "North", "East", "South", or "West". The constraints, including up to 10^5 steps per step call and 10^4 total method calls, imply that a naive simulation moving one step at a time is too slow. Edge cases to consider include the robot reaching a corner, where multiple rotations may occur in a single step call, and when the robot has to traverse along the perimeter repeatedly.
Approaches
The brute-force approach simulates each step individually, checking boundaries and turning counterclockwise when hitting an edge. This guarantees correctness because each step follows the problem rules exactly. However, since a single call may ask the robot to move up to 10^5 steps and there may be 10^4 calls, this can lead to up to 10^9 iterations, which is infeasible.
The key observation for an optimal approach is that the robot only moves along the perimeter of the grid after the first rotation, forming a fixed cyclic path along the rectangle edges. This allows precomputing the path as a list of coordinates and directions, enabling the robot to advance num steps by moving modulo the perimeter length, rather than simulating every step. The modulo operation ensures that very large step counts wrap around efficiently, and the position and direction can be retrieved in constant time.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(num) per step) |
O(1) | Simulate each step individually, too slow for large num |
| Optimal | O(1) per step after preprocessing |
O(width + height) | Precompute perimeter path, use modulo arithmetic to skip large step counts |
Algorithm Walkthrough
- Initialization: Store the grid dimensions
widthandheight. Create listsperimeteranddirectionsthat represent the robot's path along the perimeter in clockwise order and the corresponding direction at each cell. - Build Perimeter Path: Starting from
(0, 0)and facing East, traverse the rectangle clockwise to generate all unique edge cells. Add each cell to theperimeterlist and the direction it will be facing at that cell to thedirectionslist. - Tracking Position: Maintain a single integer
pos_indexthat points to the robot’s current location in theperimeterlist. - Step Function: When
step(num)is called, updatepos_indexas(pos_index + num) % len(perimeter). This efficiently moves the robotnumsteps along the perimeter path in O(1) time. - Get Position: Return the coordinate from
perimeter[pos_index]. - Get Direction: Return the direction from
directions[pos_index].
Why it works: The perimeter path captures all valid positions and directions the robot can occupy along the rectangle. Since the robot always follows the edge after the first rotation, moving by num modulo the perimeter length reproduces exactly the robot’s state after num steps.
Python Solution
from typing import List
class Robot:
def __init__(self, width: int, height: int):
self.width = width
self.height = height
self.perimeter = []
self.directions = []
# Precompute the perimeter clockwise starting East
for x in range(width):
self.perimeter.append((x, 0))
self.directions.append("East")
for y in range(1, height):
self.perimeter.append((width - 1, y))
self.directions.append("North")
for x in range(width - 2, -1, -1):
self.perimeter.append((x, height - 1))
self.directions.append("West")
for y in range(height - 2, 0, -1):
self.perimeter.append((0, y))
self.directions.append("South")
self.pos_index = 0
def step(self, num: int) -> None:
if self.pos_index == 0 and num > 0:
# Avoid "East" reset at start after first rotation
self.pos_index = (self.pos_index + num - 1) % len(self.perimeter)
self.pos_index += 1
else:
self.pos_index = (self.pos_index + num) % len(self.perimeter)
def getPos(self) -> List[int]:
return list(self.perimeter[self.pos_index])
def getDir(self) -> str:
return self.directions[self.pos_index]
The Python implementation precomputes the perimeter path and directions. In step, we handle a subtle case where the robot starts at (0, 0) and moves, ensuring the first turn is correctly represented. Both getPos and getDir simply retrieve precomputed values in O(1) time.
Go Solution
type Robot struct {
width, height int
perimeter [][2]int
directions []string
posIndex int
}
func Constructor(width int, height int) Robot {
r := Robot{
width: width,
height: height,
perimeter: make([][2]int, 0),
directions: make([]string, 0),
posIndex: 0,
}
for x := 0; x < width; x++ {
r.perimeter = append(r.perimeter, [2]int{x, 0})
r.directions = append(r.directions, "East")
}
for y := 1; y < height; y++ {
r.perimeter = append(r.perimeter, [2]int{width - 1, y})
r.directions = append(r.directions, "North")
}
for x := width - 2; x >= 0; x-- {
r.perimeter = append(r.perimeter, [2]int{x, height - 1})
r.directions = append(r.directions, "West")
}
for y := height - 2; y > 0; y-- {
r.perimeter = append(r.perimeter, [2]int{0, y})
r.directions = append(r.directions, "South")
}
return r
}
func (r *Robot) Step(num int) {
if r.posIndex == 0 && num > 0 {
r.posIndex = (r.posIndex + num - 1) % len(r.perimeter)
r.posIndex++
} else {
r.posIndex = (r.posIndex + num) % len(r.perimeter)
}
}
func (r *Robot) GetPos() []int {
return []int{r.perimeter[r.posIndex][0], r.perimeter[r.posIndex][1]}
}
func (r *Robot) GetDir() string {
return r.directions[r.posIndex]
}
In Go, the approach is identical. We use slices for perimeter and directions, and posIndex tracks the current location. The handling of the first step from (0,0) mirrors Python.
Worked Examples
For the example:
Robot robot = new Robot(6, 3);
robot.step(2); // Moves to (2,0), East
robot.step(2); // Moves to (4,0), East
robot.getPos(); // [4,0]
robot.getDir(); // "East"
robot.step(2); // Moves to (5,0), East -> turn North -> (5,1), North
robot.step(1); // (5,2), North
robot.step(4); // North blocked -> West -> (1,2), West
robot.getPos(); // [1,2]
robot.getDir(); // "West"
Perimeter list:
[(0,0),(1,0),(2,0),(3,0),(4,0),(5,0),(5,1),(5,2),(4,2),(3,2),(2,2),(1,2),(0,2),(0,1)]
directions list mirrors the robot’s facing at each cell.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(1) per step) |
Precomputed perimeter allows modulo arithmetic, no iteration per step |
| Space | O(width + height) | Store perimeter path and directions along rectangle edges |
The preprocessing is O(width + height) but is acceptable due to the grid constraints (≤ 100).
Test Cases
robot = Robot(6, 3)
robot.step(2)
assert robot.getPos() == [2, 0] # Moves East
assert robot.getDir() == "East"
robot.step(2)
assert robot.getPos() == [4