LeetCode 第353题:贪吃蛇游戏
📖 文章摘要
本文详细解析LeetCode第353题"贪吃蛇游戏",这是一道考察数据结构设计和游戏逻辑的中等难度题目。文章提供了基于双端队列和哈希集合的解法,包含C#、Java、Python三种语言实现,配有详细的思路分析和性能对比。适合想要深入理解数据结构和游戏设计的程序员。
核心知识点: 双端队列、哈希集合、游戏设计
难度等级: 中等
推荐人群: 数据结构进阶学习者、游戏开发者
题目描述
请你设计一个贪吃蛇游戏,该游戏将会在一个 屏幕尺寸 = 宽度 x 高度
的屏幕上运行。
起初时,蛇在左上角的 (0, 0)
位置,身体长度为 1 个单位。你将会被给出一个 (行, 列)
形式的食物位置序列。
实现 SnakeGame
类:
SnakeGame(int width, int height, int[][] food)
初始化对象,屏幕大小为height x width
,食物位置序列为food
int move(String direction)
返回蛇在移动后的得分。如果蛇吃到食物,得分加一。如果蛇与自己相撞或者撞墙,返回 -1。
示例
输入:
["SnakeGame", "move", "move", "move", "move", "move", "move"]
[[3, 2, [[1, 2], [0, 1]]], ["R"], ["D"], ["R"], ["U"], ["L"], ["U"]]
输出:
[null, 0, 0, 1, 1, 2, -1]
解释:
SnakeGame snakeGame = new SnakeGame(3, 2, [[1, 2], [0, 1]]);
snakeGame.move("R"); // 返回 0
snakeGame.move("D"); // 返回 0
snakeGame.move("R"); // 返回 1,蛇吃掉了第一个食物,位于 (1, 2)
snakeGame.move("U"); // 返回 1
snakeGame.move("L"); // 返回 2,蛇吃掉了第二个食物,位于 (0, 1)
snakeGame.move("U"); // 返回 -1,蛇与边界相撞,游戏结束
提示
1 <= width, height <= 104
1 <= food.length <= 50
food[i].length == 2
0 <= food[i][0] < height
0 <= food[i][1] < width
direction
字符串为{"U","D","L","R"}
之一- 最多调用
104
次move
方法
解题思路
本题的核心是使用合适的数据结构来维护蛇的身体位置和移动状态。主要解法如下:
-
使用双端队列(Deque)存储蛇的身体
- 队列头部表示蛇头位置
- 队列尾部表示蛇尾位置
- 每次移动时更新队列
-
使用哈希集合(HashSet)存储蛇身位置
- 快速判断是否与自身相撞
- 维护当前蛇占据的所有格子
-
移动逻辑处理:
- 计算蛇头的下一个位置
- 检查是否撞墙或自身
- 判断是否吃到食物
- 更新蛇的身体位置
代码实现
C# 实现
public class SnakeGame {
private int width;
private int height;
private int[][] food;
private int foodIndex;
private int score;
private LinkedList<(int, int)> snake;
private HashSet<(int, int)> snakeSet;
public SnakeGame(int width, int height, int[][] food) {
this.width = width;
this.height = height;
this.food = food;
this.foodIndex = 0;
this.score = 0;
this.snake = new LinkedList<(int, int)>();
this.snakeSet = new HashSet<(int, int)>();
// 初始位置
snake.AddFirst((0, 0));
snakeSet.Add((0, 0));
}
public int Move(string direction) {
// 获取蛇头位置
var head = snake.First.Value;
int newRow = head.Item1;
int newCol = head.Item2;
// 根据方向移动
switch (direction) {
case "U": newRow--; break;
case "D": newRow++; break;
case "L": newCol--; break;
case "R": newCol++; break;
}
// 检查是否撞墙
if (newRow < 0 || newRow >= height || newCol < 0 || newCol >= width) {
return -1;
}
// 移除蛇尾
var tail = snake.Last.Value;
snake.RemoveLast();
snakeSet.Remove(tail);
// 检查是否撞到自己
if (snakeSet.Contains((newRow, newCol))) {
return -1;
}
// 添加新的蛇头
snake.AddFirst((newRow, newCol));
snakeSet.Add((newRow, newCol));
// 检查是否吃到食物
if (foodIndex < food.Length &&
newRow == food[foodIndex][0] &&
newCol == food[foodIndex][1]) {
// 吃到食物,加回蛇尾
snake.AddLast(tail);
snakeSet.Add(tail);
foodIndex++;
score++;
}
return score;
}
}
Java 实现
class SnakeGame {
private int width;
private int height;
private int[][] food;
private int foodIndex;
private int score;
private Deque<int[]> snake;
private Set<String> snakeSet;
public SnakeGame(int width, int height, int[][] food) {
this.width = width;
this.height = height;
this.food = food;
this.foodIndex = 0;
this.score = 0;
this.snake = new LinkedList<>();
this.snakeSet = new HashSet<>();
// 初始位置
snake.offerFirst(new int[]{0, 0});
snakeSet.add("0,0");
}
public int move(String direction) {
// 获取蛇头位置
int[] head = snake.peekFirst();
int newRow = head[0];
int newCol = head[1];
// 根据方向移动
switch (direction) {
case "U": newRow--; break;
case "D": newRow++; break;
case "L": newCol--; break;
case "R": newCol++; break;
}
// 检查是否撞墙
if (newRow < 0 || newRow >= height || newCol < 0 || newCol >= width) {
return -1;
}
// 移除蛇尾
int[] tail = snake.pollLast();
snakeSet.remove(tail[0] + "," + tail[1]);
// 检查是否撞到自己
String newPos = newRow + "," + newCol;
if (snakeSet.contains(newPos)) {
return -1;
}
// 添加新的蛇头
snake.offerFirst(new int[]{newRow, newCol});
snakeSet.add(newPos);
// 检查是否吃到食物
if (foodIndex < food.length &&
newRow == food[foodIndex][0] &&
newCol == food[foodIndex][1]) {
// 吃到食物,加回蛇尾
snake.offerLast(tail);
snakeSet.add(tail[0] + "," + tail[1]);
foodIndex++;
score++;
}
return score;
}
}
Python 实现
from collections import deque
class SnakeGame:
def __init__(self, width: int, height: int, food: List[List[int]]):
self.width = width
self.height = height
self.food = food
self.food_index = 0
self.score = 0
self.snake = deque([(0, 0)]) # 蛇的身体位置
self.snake_set = {(0, 0)} # 蛇身占据的格子
def move(self, direction: str) -> int:
# 获取蛇头位置
head_row, head_col = self.snake[0]
# 根据方向移动
if direction == "U": head_row -= 1
elif direction == "D": head_row += 1
elif direction == "L": head_col -= 1
else: head_col += 1 # "R"
# 检查是否撞墙
if (head_row < 0 or head_row >= self.height or
head_col < 0 or head_col >= self.width):
return -1
# 移除蛇尾
tail = self.snake.pop()
self.snake_set.remove(tail)
# 检查是否撞到自己
new_head = (head_row, head_col)
if new_head in self.snake_set:
return -1
# 添加新的蛇头
self.snake.appendleft(new_head)
self.snake_set.add(new_head)
# 检查是否吃到食物
if (self.food_index < len(self.food) and
head_row == self.food[self.food_index][0] and
head_col == self.food[self.food_index][1]):
# 吃到食物,加回蛇尾
self.snake.append(tail)
self.snake_set.add(tail)
self.food_index += 1
self.score += 1
return self.score
复杂度分析
时间复杂度
- 初始化:O(1)
- 移动操作:O(1)
- 双端队列的头尾操作都是O(1)
- 哈希集合的查询和更新也是O(1)
空间复杂度
- O(N),其中N是蛇的最大长度
- 双端队列存储蛇的身体位置
- 哈希集合存储蛇身占据的格子
代码亮点
- 🎯 使用双端队列高效管理蛇的身体
- 💡 使用哈希集合快速判断碰撞
- 🔍 清晰的移动逻辑处理流程
- 🎨 代码结构模块化,易于维护
常见错误分析
- 🚫 忘记更新蛇尾位置
- 🚫 判断自身碰撞的时机错误
- 🚫 食物序列索引处理不当
- 🚫 边界条件检查不完整
相关题目
解题技巧总结
- 使用双端队列维护动态变化的序列
- 使用哈希集合优化查找操作
- 分步处理复杂的游戏逻辑
- 注意边界条件和特殊情况
📖 系列导航
🔥 算法专题合集 - 查看完整合集
📢 关注合集更新:点击上方合集链接,关注获取最新题解!目前已更新至第353题。
💬 互动交流
感谢大家耐心阅读到这里!希望这篇题解能够帮助你更好地理解和掌握这道算法题。
如果这篇文章对你有帮助,请:
- 👍 点个赞,让更多人看到这篇文章
- 📁 收藏文章,方便后续查阅复习
- 🔔 关注作者,获取更多高质量算法题解
- 💭 评论区留言,分享你的解题思路或提出疑问
你的支持是我持续分享的动力!
💡 一起进步:算法学习路上不孤单,欢迎一起交流学习!