Prerequisites
Before attempting this problem, you should be comfortable with:
- Deque (Double-Ended Queue) - Adding to front and removing from back in O(1) time
- Hash Set - O(1) lookup for collision detection
- 2D Coordinate Systems - Working with row/column positions and boundary checking
1. Queue and Hash Set
Intuition
The snake game requires tracking the snake's body as it moves and grows. A deque (double-ended queue) is perfect because we add to the front (new head position) and remove from the back (tail moves forward). To quickly check whether the snake bites itself, we also maintain a hash set of all occupied cells. When the snake eats food, the tail stays in place so the snake grows. The score equals the number of food items eaten, which is the snake length minus 1.
Algorithm
- Initialization: Create a deque with the snake starting at
(0, 0). Add this position to a hash set. Store the grid dimensions, food list, and a food index starting at0. Define movement deltas for U, D, L, R. - move(direction): Calculate the new head position by applying the direction delta.
- Check if the new head is out of bounds. If so, return
-1. - Check if the new head collides with the snake body (exists in the hash set and is not the current tail). If so, return
-1. - If there is food at the new head position, increment the food index (the tail stays, so the snake grows).
- Otherwise, remove the tail from the deque and the hash set (the snake moves without growing).
- Add the new head to the front of the deque and to the hash set.
- Return the current score (snake length minus
1).
class SnakeGame:
def __init__(self, width: int, height: int, food: List[List[int]]):
self.snake = collections.deque([(0,0)]) # snake head is at the front
self.snake_set = {(0,0) : 1}
self.width = width
self.height = height
self.food = food
self.food_index = 0
self.movement = {'U': [-1, 0], 'L': [0, -1], 'R': [0, 1], 'D': [1, 0]}
def move(self, direction: str) -> int:
newHead = (self.snake[0][0] + self.movement[direction][0],
self.snake[0][1] + self.movement[direction][1])
# Boundary conditions.
crosses_boundary1 = newHead[0] < 0 or newHead[0] >= self.height
crosses_boundary2 = newHead[1] < 0 or newHead[1] >= self.width
# Checking if the snake bites itself.
bites_itself = newHead in self.snake_set and newHead != self.snake[-1]
# If any of the terminal conditions are satisfied, then we exit with rcode -1.
if crosses_boundary1 or crosses_boundary2 or bites_itself:
return -1
# Note the food list could be empty at this point.
next_food_item = self.food[self.food_index] if self.food_index < len(self.food) else None
# If there's an available food item and it is on the cell occupied by the snake after the move, eat it
if self.food_index < len(self.food) and \
next_food_item[0] == newHead[0] and \
next_food_item[1] == newHead[1]: # eat food
self.food_index += 1
else: # not eating food: delete tail
tail = self.snake.pop()
del self.snake_set[tail]
# A new head always gets added
self.snake.appendleft(newHead)
# Also add the head to the set
self.snake_set[newHead] = 1
return len(self.snake) - 1Time & Space Complexity
Time Complexity:
- The time complexity of the
movefunction is . - The time taken to calculate
bites_itselfis constant since we are using a dictionary to search for the element. - The time taken to add and remove an element from the queue is also constant.
- The time complexity of the
Space Complexity:
- is used by the
fooddata structure. - is used by the
snakeand thesnake_setdata structures. At most, we can have snake that occupies all the cells of the grid.
- is used by the
Where represents the width of the grid, represents the height of the grid, and represents the number of food items in the list.
Common Pitfalls
Confusing Width and Height with Row and Column
The grid is defined as width x height, but positions are given as [row, col] where row corresponds to height and column corresponds to width. Mixing these up causes incorrect boundary checks.
# Wrong: swapped width/height in boundary check
crosses_boundary1 = newHead[0] < 0 or newHead[0] >= self.width # Should be height
crosses_boundary2 = newHead[1] < 0 or newHead[1] >= self.height # Should be width
# Correct
crosses_boundary1 = newHead[0] < 0 or newHead[0] >= self.height # row vs height
crosses_boundary2 = newHead[1] < 0 or newHead[1] >= self.width # col vs widthNot Excluding Tail When Checking Self-Collision
When the snake moves without eating, the tail vacates its position. The new head can legally occupy the old tail position. Failing to exclude the tail from collision detection causes false game-overs.
# Wrong: doesn't account for tail moving
bites_itself = newHead in self.snake_set
# Correct: tail position will be vacated (unless eating)
bites_itself = newHead in self.snake_set and newHead != self.snake[-1]Wrong Order of Operations When Eating Food
When eating food, you must not remove the tail (so the snake grows). When not eating, you must remove the tail before adding the new head. Getting this order wrong causes the snake to grow incorrectly or the set to become inconsistent.
# Wrong: always removes tail, then checks food
tail = self.snake.pop()
del self.snake_set[tail]
if is_food:
# Too late - tail already removed!
pass
# Correct: check food first, only remove tail if not eating
if is_food:
self.food_index += 1
else:
tail = self.snake.pop()
del self.snake_set[tail]
self.snake.appendleft(newHead)
self.snake_set[newHead] = 1