LeetCode 353: Design Snake Game (Deque + Hash Set)
LeetCode 353DesignSimulationToday we solve LeetCode 353 - Design Snake Game.
Source: https://leetcode.com/problems/design-snake-game/
English
Problem Summary
Design a snake game on a 2D grid. Each move updates the snake head by direction, may eat food to grow, and returns score or -1 if dead.
Key Insight
Use a deque for body order (head to tail) and a hash set for O(1) collision checks. Before self-collision check, remove tail if not eating, because moving into old tail is legal.
Algorithm Steps
1) Compute next head from direction.
2) If out of bounds, return -1.
3) Decide whether next cell is food.
4) If not eating, pop tail from deque and set.
5) If new head in set, return -1.
6) Push new head; if eating, score++.
Complexity Analysis
Each move is O(1) average time, and space is O(w*h) in worst case.
Reference Implementations (Java / Go / C++ / Python / JavaScript)
class SnakeGame {
private final int width, height;
private final int[][] food;
private int foodIndex;
private int score;
private final Deque<int[]> body;
private final Set<Integer> occupied;
public SnakeGame(int width, int height, int[][] food) {
this.width = width;
this.height = height;
this.food = food;
this.foodIndex = 0;
this.score = 0;
this.body = new ArrayDeque<>();
this.occupied = new HashSet<>();
body.addFirst(new int[]{0, 0});
occupied.add(0);
}
public int move(String direction) {
int[] head = body.peekFirst();
int nr = head[0], nc = head[1];
if (direction.equals("U")) nr--;
else if (direction.equals("D")) nr++;
else if (direction.equals("L")) nc--;
else nc++;
if (nr < 0 || nr >= height || nc < 0 || nc >= width) return -1;
boolean eat = foodIndex < food.length && nr == food[foodIndex][0] && nc == food[foodIndex][1];
if (!eat) {
int[] tail = body.removeLast();
occupied.remove(tail[0] * width + tail[1]);
}
int key = nr * width + nc;
if (occupied.contains(key)) return -1;
body.addFirst(new int[]{nr, nc});
occupied.add(key);
if (eat) {
foodIndex++;
score++;
}
return score;
}
}type SnakeGame struct {
width, height int
food [][]int
fi, score int
body [][2]int
occ map[int]bool
}
func Constructor(width int, height int, food [][]int) SnakeGame {
g := SnakeGame{width: width, height: height, food: food, occ: map[int]bool{0: true}}
g.body = append(g.body, [2]int{0, 0})
return g
}
func (g *SnakeGame) Move(direction string) int {
head := g.body[0]
nr, nc := head[0], head[1]
switch direction { case "U": nr--; case "D": nr++; case "L": nc--; default: nc++ }
if nr < 0 || nr >= g.height || nc < 0 || nc >= g.width { return -1 }
eat := g.fi < len(g.food) && nr == g.food[g.fi][0] && nc == g.food[g.fi][1]
if !eat {
tail := g.body[len(g.body)-1]
g.body = g.body[:len(g.body)-1]
delete(g.occ, tail[0]*g.width+tail[1])
}
k := nr*g.width + nc
if g.occ[k] { return -1 }
g.body = append([][2]int{{nr, nc}}, g.body...)
g.occ[k] = true
if eat { g.fi++; g.score++ }
return g.score
}class SnakeGame {
int width, height, foodIdx = 0, score = 0;
vector<vector<int>> food;
deque<pair<int,int>> body;
unordered_set<int> occ;
public:
SnakeGame(int width, int height, vector<vector<int>>& food): width(width), height(height), food(food) {
body.push_front({0,0});
occ.insert(0);
}
int move(string direction) {
auto [r, c] = body.front();
if (direction == "U") r--; else if (direction == "D") r++; else if (direction == "L") c--; else c++;
if (r < 0 || r >= height || c < 0 || c >= width) return -1;
bool eat = (foodIdx < (int)food.size() && r == food[foodIdx][0] && c == food[foodIdx][1]);
if (!eat) {
auto [tr, tc] = body.back();
body.pop_back();
occ.erase(tr * width + tc);
}
int k = r * width + c;
if (occ.count(k)) return -1;
body.push_front({r, c});
occ.insert(k);
if (eat) { foodIdx++; score++; }
return score;
}
};from collections import deque
class SnakeGame:
def __init__(self, width: int, height: int, food: list[list[int]]):
self.w = width
self.h = height
self.food = food
self.fi = 0
self.score = 0
self.body = deque([(0, 0)])
self.occ = {0}
def move(self, direction: str) -> int:
r, c = self.body[0]
if direction == "U": r -= 1
elif direction == "D": r += 1
elif direction == "L": c -= 1
else: c += 1
if r < 0 or r >= self.h or c < 0 or c >= self.w:
return -1
eat = self.fi < len(self.food) and [r, c] == self.food[self.fi]
if not eat:
tr, tc = self.body.pop()
self.occ.remove(tr * self.w + tc)
k = r * self.w + c
if k in self.occ:
return -1
self.body.appendleft((r, c))
self.occ.add(k)
if eat:
self.fi += 1
self.score += 1
return self.scoreclass SnakeGame {
constructor(width, height, food) {
this.w = width;
this.h = height;
this.food = food;
this.fi = 0;
this.score = 0;
this.body = [[0, 0]];
this.occ = new Set([0]);
}
move(direction) {
let [r, c] = this.body[0];
if (direction === "U") r--;
else if (direction === "D") r++;
else if (direction === "L") c--;
else c++;
if (r < 0 || r >= this.h || c < 0 || c >= this.w) return -1;
const eat = this.fi < this.food.length && r === this.food[this.fi][0] && c === this.food[this.fi][1];
if (!eat) {
const [tr, tc] = this.body.pop();
this.occ.delete(tr * this.w + tc);
}
const k = r * this.w + c;
if (this.occ.has(k)) return -1;
this.body.unshift([r, c]);
this.occ.add(k);
if (eat) { this.fi++; this.score++; }
return this.score;
}
}中文
题目概述
在二维网格中实现贪吃蛇。每次移动后返回当前分数;若撞墙或撞到自己,返回 -1。
核心思路
用双端队列保存蛇身顺序,用哈希集合保存占用格子做 O(1) 碰撞判断。关键细节是,不吃食物时要先移除尾巴,再判断是否撞到自己。
算法步骤
1)根据方向计算新蛇头。
2)越界直接返回 -1。
3)判断是否吃到当前食物。
4)若没吃到,先弹出尾巴并从集合移除。
5)新头若已在集合中则返回 -1。
6)插入新头;若吃到食物则分数加一。
复杂度分析
单次 move 平均时间复杂度 O(1),空间复杂度最坏 O(w*h)。
多语言参考实现(Java / Go / C++ / Python / JavaScript)
class SnakeGame {
private final int width, height;
private final int[][] food;
private int foodIndex;
private int score;
private final Deque<int[]> body;
private final Set<Integer> occupied;
public SnakeGame(int width, int height, int[][] food) {
this.width = width;
this.height = height;
this.food = food;
this.foodIndex = 0;
this.score = 0;
this.body = new ArrayDeque<>();
this.occupied = new HashSet<>();
body.addFirst(new int[]{0, 0});
occupied.add(0);
}
public int move(String direction) {
int[] head = body.peekFirst();
int nr = head[0], nc = head[1];
if (direction.equals("U")) nr--;
else if (direction.equals("D")) nr++;
else if (direction.equals("L")) nc--;
else nc++;
if (nr < 0 || nr >= height || nc < 0 || nc >= width) return -1;
boolean eat = foodIndex < food.length && nr == food[foodIndex][0] && nc == food[foodIndex][1];
if (!eat) {
int[] tail = body.removeLast();
occupied.remove(tail[0] * width + tail[1]);
}
int key = nr * width + nc;
if (occupied.contains(key)) return -1;
body.addFirst(new int[]{nr, nc});
occupied.add(key);
if (eat) {
foodIndex++;
score++;
}
return score;
}
}type SnakeGame struct {
width, height int
food [][]int
fi, score int
body [][2]int
occ map[int]bool
}
func Constructor(width int, height int, food [][]int) SnakeGame {
g := SnakeGame{width: width, height: height, food: food, occ: map[int]bool{0: true}}
g.body = append(g.body, [2]int{0, 0})
return g
}
func (g *SnakeGame) Move(direction string) int {
head := g.body[0]
nr, nc := head[0], head[1]
switch direction { case "U": nr--; case "D": nr++; case "L": nc--; default: nc++ }
if nr < 0 || nr >= g.height || nc < 0 || nc >= g.width { return -1 }
eat := g.fi < len(g.food) && nr == g.food[g.fi][0] && nc == g.food[g.fi][1]
if !eat {
tail := g.body[len(g.body)-1]
g.body = g.body[:len(g.body)-1]
delete(g.occ, tail[0]*g.width+tail[1])
}
k := nr*g.width + nc
if g.occ[k] { return -1 }
g.body = append([][2]int{{nr, nc}}, g.body...)
g.occ[k] = true
if eat { g.fi++; g.score++ }
return g.score
}class SnakeGame {
int width, height, foodIdx = 0, score = 0;
vector<vector<int>> food;
deque<pair<int,int>> body;
unordered_set<int> occ;
public:
SnakeGame(int width, int height, vector<vector<int>>& food): width(width), height(height), food(food) {
body.push_front({0,0});
occ.insert(0);
}
int move(string direction) {
auto [r, c] = body.front();
if (direction == "U") r--; else if (direction == "D") r++; else if (direction == "L") c--; else c++;
if (r < 0 || r >= height || c < 0 || c >= width) return -1;
bool eat = (foodIdx < (int)food.size() && r == food[foodIdx][0] && c == food[foodIdx][1]);
if (!eat) {
auto [tr, tc] = body.back();
body.pop_back();
occ.erase(tr * width + tc);
}
int k = r * width + c;
if (occ.count(k)) return -1;
body.push_front({r, c});
occ.insert(k);
if (eat) { foodIdx++; score++; }
return score;
}
};from collections import deque
class SnakeGame:
def __init__(self, width: int, height: int, food: list[list[int]]):
self.w = width
self.h = height
self.food = food
self.fi = 0
self.score = 0
self.body = deque([(0, 0)])
self.occ = {0}
def move(self, direction: str) -> int:
r, c = self.body[0]
if direction == "U": r -= 1
elif direction == "D": r += 1
elif direction == "L": c -= 1
else: c += 1
if r < 0 or r >= self.h or c < 0 or c >= self.w:
return -1
eat = self.fi < len(self.food) and [r, c] == self.food[self.fi]
if not eat:
tr, tc = self.body.pop()
self.occ.remove(tr * self.w + tc)
k = r * self.w + c
if k in self.occ:
return -1
self.body.appendleft((r, c))
self.occ.add(k)
if eat:
self.fi += 1
self.score += 1
return self.scoreclass SnakeGame {
constructor(width, height, food) {
this.w = width;
this.h = height;
this.food = food;
this.fi = 0;
this.score = 0;
this.body = [[0, 0]];
this.occ = new Set([0]);
}
move(direction) {
let [r, c] = this.body[0];
if (direction === "U") r--;
else if (direction === "D") r++;
else if (direction === "L") c--;
else c++;
if (r < 0 || r >= this.h || c < 0 || c >= this.w) return -1;
const eat = this.fi < this.food.length && r === this.food[this.fi][0] && c === this.food[this.fi][1];
if (!eat) {
const [tr, tc] = this.body.pop();
this.occ.delete(tr * this.w + tc);
}
const k = r * this.w + c;
if (this.occ.has(k)) return -1;
this.body.unshift([r, c]);
this.occ.add(k);
if (eat) { this.fi++; this.score++; }
return this.score;
}
}
Comments