1. 项目概述与核心价值
最近在技术社区和求职圈里,“华为OD机考”的热度一直居高不下,尤其是其中的C卷真题,常常被看作是检验开发者综合编程能力的一块“试金石”。今天要拆解的这道“贪吃蛇”题目,就是一道典型的200分大题。它远不止是让你写一个童年游戏那么简单,而是融合了模拟、状态机、边界判断和算法设计的综合应用题。很多朋友初次看到题目可能会觉得“贪吃蛇谁不会写”,但真上手实现时,才会发现题目中埋藏的诸多细节和边界条件,足以让代码从“能跑”到“稳健”拉开巨大差距。
这道题的核心价值在于,它模拟了一个经典的、有明确规则的系统,要求开发者具备将自然语言描述的业务逻辑,精准、无歧义地转化为代码的能力。这恰恰是软件开发,尤其是业务系统开发中最核心的能力之一。通过实现它,你不仅能巩固Java基础语法,更能深入理解面向对象设计、程序状态管理和复杂条件判断的实践技巧。无论你是正在备战华为OD机试,还是想找一道高质量的题目来锻炼自己的工程化编码思维,这道“贪吃蛇”都是一个绝佳的选择。
接下来,我将从一个经历过多次类似机考和实际项目开发的视角,带你完整拆解这道题的解题思路、代码实现,并分享那些只有踩过坑才能知道的注意事项和优化技巧。我们会从理解题意开始,一步步构建出清晰的数据模型,最终实现一个逻辑严密、鲁棒性强的解决方案。
2. 题目深度解析与建模思路
拿到一道机试题,最忌讳的就是还没完全理解题意就开始敲代码。对于“贪吃蛇”这类模拟题,第一步必须是彻底吃透题目描述,抽象出核心的数据模型和状态规则。
2.1 问题场景与规则定义
通常,这类题目的描述会包含以下几个关键部分,我们需要逐一提取并明确:
- 游戏场地:一个
M x N的网格。我们需要用二维数组(或类似结构)来表示。网格上的每个坐标(x, y)可能的状态有:空地、蛇身、食物。这里x通常代表行,y代表列,索引一般从0或1开始,这是第一个需要明确的细节。 - 蛇的初始状态:题目会给出蛇的初始长度和位置。通常蛇身是一个坐标序列,例如
[(x1,y1), (x2,y2), ...]。我们需要确定这个序列的顺序:头部是第一个元素还是最后一个元素?这决定了移动时如何更新身体。 - 移动指令序列:一串由字符组成的命令,例如
“URDL”,分别代表上(Up)、右(Right)、下(Down)、左(Left)。我们需要按顺序处理这些指令。 - 核心游戏规则:
- 正常移动:蛇头向指定方向移动一格。原蛇头变为新的身体第一节,原蛇尾向前移动(即从序列中移除末尾坐标)。这模拟了蛇的爬行。
- 吃到食物:如果移动后蛇头到达的食物所在坐标,则蛇长度增加。此时,蛇尾不移动,新的蛇头坐标加入序列头部,食物被消耗并在随机(或指定)位置生成新的食物。但机考题为了简化,常常是给定固定的食物坐标序列或单一食物,吃完即游戏结束或进入下一阶段。
- 游戏结束条件(死亡判定):这是最容易出错的地方,必须全面考虑:
- 撞墙:蛇头移动后超出网格边界。
- 撞到自己:蛇头移动后,新的头部坐标与当前蛇身的任何一个部分(通常不包括原尾部,因为尾部会移开)重合。
- 指令耗尽:所有指令执行完毕,蛇依然存活。此时需要输出蛇的最终长度或位置。
注意:机考题的描述可能不会像上面这么规整。它可能会用一段话描述,里面夹杂着所有信息。我的习惯是拿出一张纸或打开注释,手动列出所有“名词”(实体,如蛇、食物、墙)和“动词”(动作,如移动、吃、死),并明确它们之间的关系和属性。
2.2 数据结构设计与选型
明确了规则,接下来就要选择合适的数据结构来承载我们的模型。这是影响代码简洁性和效率的关键。
游戏地图 (
board):- 方案一:二维整型数组。用不同的数字标记状态,例如
0-空地,1-蛇身,2-食物。优点是判断速度快(O(1)),直观。缺点是更新蛇身移动时,需要擦除旧蛇尾、绘制新蛇头,操作稍显繁琐。 - 方案二:仅用集合记录蛇身和食物坐标。地图边界通过坐标值判断。例如,蛇身坐标保存在一个有序集合(如
LinkedList)中,食物坐标用一个Point对象或简单的int[]存储。判断是否撞到自己,就是判断新蛇头坐标是否在蛇身集合中(注意排除即将移走的蛇尾)。这个方案更贴近“对象”思维,代码更清晰,是我更推荐的做法。我们后续实现将采用此方案。
- 方案一:二维整型数组。用不同的数字标记状态,例如
蛇的表示 (
snake):- 必须选择一个能维护顺序、能快速在头部插入和尾部删除的数据结构。
LinkedList<int[]>或LinkedList<Point>是完美选择。链表头部代表蛇头,尾部代表蛇尾。移动时,在头部插入新坐标,并移除尾部坐标(如果没吃到食物)。所有操作的时间复杂度都是O(1)。- 使用
Deque(双端队列)接口来引用LinkedList是更优雅的做法,因为它明确了“头部”和“尾部”的操作语义。
食物表示 (
food):- 如果只有一个食物,一个简单的
int[]即可。 - 如果是一系列食物(按顺序出现),可以用一个队列
Queue<int[]>来存储。
- 如果只有一个食物,一个简单的
方向移动:
- 定义一个枚举
Direction包含U, R, D, L。 - 使用两个数组
dx = {-1, 0, 1, 0}和dy = {0, 1, 0, -1}来映射方向到坐标的变化。这是处理网格移动的经典技巧,能避免冗长的if-else或switch语句。
- 定义一个枚举
2.3 核心算法流程设计
有了数据结构,算法流程就清晰了。我们可以将其设计为一个状态机:
初始化地图、蛇、食物 for (每个移动指令 char cmd) { 1. 根据 cmd 确定移动方向 dir。 2. 计算蛇头的新坐标 newHead。 3. 死亡判定: a. 是否撞墙?(newHead 超出边界) b. 是否撞到自己?(newHead 存在于当前蛇身集合中,且不等于蛇尾?这里需要仔细斟酌) 4. 如果死亡,游戏结束,返回当前长度或-1。 5. 将 newHead 加入蛇身序列的头部。 6. 判断 newHead 是否等于食物坐标: a. 如果是,吃掉食物。蛇长度+1。更新食物位置(从队列取下一个或标记已吃)。 b. 如果不是,移除蛇身序列的尾部(蛇尾前移)。 7. 更新蛇身坐标集合(如果用了集合辅助判断)。 } 循环结束,所有指令执行完毕,蛇存活。返回最终蛇的长度。这里第3.b步“撞到自己”的判断需要特别注意。在移动的瞬间,蛇尾会移开。所以,新蛇头允许和当前蛇尾重合吗?这取决于题目定义。在大多数经典规则中,如果蛇只是向前移动一格(没有转向),那么新头会占据旧尾的位置,这是允许的,因为旧尾已经离开了。但如果蛇转向了,新头可能会撞到身体的其它部分。一个安全的做法是:在移除蛇尾之前,先判断新头是否与蛇身(用一个Set存储所有身体坐标)重合。但这样会把即将移走的蛇尾也算进去。因此,更精确的做法是:先计算新头,然后判断新头是否在“当前蛇身坐标集合”中,并且新头不等于当前蛇尾坐标。如果等于蛇尾,且是正常移动(非转向导致的回头),则是允许的。为了简化,许多实现采用一个“偷懒”但有效的办法:在移动前,先将当前蛇尾从坐标集合中移除,然后再判断新头是否在集合中,最后再根据是否吃到食物决定是否把旧尾加回来。这个技巧能巧妙地处理蛇尾问题。
3. Java代码实现与逐行解读
理论清晰后,我们开始动手实现。我会采用面向对象的思想,将游戏状态封装在一个类里,并使用清晰的数据结构。
3.1 核心类与常量定义
import java.util.*; public class HuaweiODGreedySnake { // 方向枚举,清晰定义指令字符与坐标偏移的映射 enum Direction { UP('U', -1, 0), RIGHT('R', 0, 1), DOWN('D', 1, 0), LEFT('L', 0, -1); char cmd; int dx; int dy; Direction(char cmd, int dx, int dy) { this.cmd = cmd; this.dx = dx; this.dy = dy; } // 根据指令字符快速查找方向对象 static Direction fromChar(char c) { for (Direction dir : values()) { if (dir.cmd == c) { return dir; } } throw new IllegalArgumentException("Invalid direction: " + c); } } // 游戏状态类 static class Game { int rows, cols; // 场地大小 Deque<int[]> snake; // 双端队列表示蛇,头部是蛇头 Set<String> bodySet; // 用HashSet快速判断撞身,存储坐标的字符串形式如"x,y" int[][] food; // 食物序列 int foodIndex; // 当前该吃的食物索引 int score; // 当前分数(蛇长度) public Game(int rows, int cols, int[][] snakeInit, int[][] food) { this.rows = rows; this.cols = cols; this.food = food; this.foodIndex = 0; this.snake = new LinkedList<>(); this.bodySet = new HashSet<>(); // 初始化蛇身。假设snakeInit是按顺序给出的坐标数组,如[[2,0], [1,0], [0,0]] for (int[] pos : snakeInit) { snake.addLast(pos.clone()); // 注意克隆,避免外部修改影响内部状态 bodySet.add(pos[0] + "," + pos[1]); } this.score = snake.size(); // 初始长度 } /** * 执行一步移动 * @param command 移动指令字符 * @return 移动后是否存活,true存活,false死亡 */ public boolean move(char command) { Direction dir = Direction.fromChar(command); int[] head = snake.peekFirst(); // 当前蛇头 int newX = head[0] + dir.dx; int newY = head[1] + dir.dy; int[] newHead = new int[]{newX, newY}; // 1. 撞墙判定 if (newX < 0 || newX >= rows || newY < 0 || newY >= cols) { return false; // 死亡 } // 2. 撞身体判定(使用移除蛇尾技巧) // 先获取当前蛇尾,并暂时从集合中移除 int[] tail = snake.peekLast(); bodySet.remove(tail[0] + "," + tail[1]); // 判断新头是否与当前身体(不含旧尾)碰撞 String newHeadKey = newX + "," + newY; if (bodySet.contains(newHeadKey)) { return false; // 死亡 } // 3. 移动有效,将新头加入 snake.addFirst(newHead); bodySet.add(newHeadKey); // 4. 判断是否吃到食物 if (foodIndex < food.length && newX == food[foodIndex][0] && newY == food[foodIndex][1]) { // 吃到食物,长度增加,食物索引+1,并且不要把旧尾加回集合(因为蛇变长了) score++; foodIndex++; // 注意:吃到食物时,蛇尾不移除,所以需要把刚才移除的旧尾加回来吗? // 不!因为旧尾现在还是蛇的一部分(身体延长了)。 // 我们之前移除它只是为了做碰撞检测,现在需要把它加回集合。 bodySet.add(tail[0] + "," + tail[1]); // 蛇的队列不需要移除尾部,所以这里什么都不做。 } else { // 没吃到食物,需要移除旧尾 snake.removeLast(); // 旧尾已经在碰撞检测前从bodySet移除了,这里不需要再操作。 // 如果没吃到,旧尾就应该消失。 } return true; // 存活 } public int getScore() { return score; } } /** * 主解题函数 * @param rows 行数 * @param cols 列数 * @param snakeInit 蛇初始身体坐标序列 * @param food 食物坐标序列 * @param commands 移动指令字符串 * @return 游戏结束后的分数(蛇长度),若中途死亡可能返回-1或初始长度,依题目要求而定。 */ public static int playGame(int rows, int cols, int[][] snakeInit, int[][] food, String commands) { Game game = new Game(rows, cols, snakeInit, food); for (char cmd : commands.toCharArray()) { if (!game.move(cmd)) { // 如果中途死亡,题目可能要求返回-1,或者死亡时的长度 // 这里根据常见要求,返回-1表示游戏失败 return -1; } } // 所有指令执行完毕,蛇依然存活,返回最终长度 return game.getScore(); } }3.2 关键代码段解析与技巧
方向枚举 (
Direction):- 使用枚举将字符指令、方向语义和坐标偏移量绑定在一起,比用多个
if-else或switch更清晰,也更容易扩展。 fromChar方法提供了从指令到方向对象的快速转换。
- 使用枚举将字符指令、方向语义和坐标偏移量绑定在一起,比用多个
蛇身的双重表示 (
Deque+HashSet):Deque<int[]>(LinkedList) 负责维护蛇身的顺序,方便在头部插入新坐标、在尾部移除旧坐标。HashSet<String>负责提供O(1)时间复杂度的存在性判断,用于检测撞到自己。将坐标(x,y)转换为字符串"x,y"作为键是一种简单有效的方法。也可以使用Set<Point>,但需要确保Point类正确重写了equals和hashCode方法。
“移除蛇尾”碰撞检测技巧:
- 这是实现中最精妙的一点。在
move方法中,我们在计算新头位置后,立即将当前蛇尾从bodySet中移除。 - 这样,后续判断新头是否在
bodySet中时,bodySet代表的就是“移动后,蛇尾离开前”的蛇身。如果新头在这个集合里,那一定是撞到了除了即将离开的蛇尾以外的身体部分,属于非法碰撞。 - 这个技巧完美处理了“新头可能刚好移动到旧尾位置”这一合法情况。
- 这是实现中最精妙的一点。在
吃到食物后的处理逻辑:
- 吃到食物时,蛇长度增加,蛇尾不应该被移除。但我们的碰撞检测已经移除了蛇尾。所以,在吃到食物的分支里,我们需要把刚才移除的蛇尾坐标加回
bodySet,因为此时它仍然是蛇身体的一部分。 - 同时,
snake队列不需要执行removeLast()操作。 - 没吃到食物时,逻辑是标准的:新头加入,旧尾移除(从队列中移除)。由于旧尾早已从
bodySet移除,这里无需再次操作。
- 吃到食物时,蛇长度增加,蛇尾不应该被移除。但我们的碰撞检测已经移除了蛇尾。所以,在吃到食物的分支里,我们需要把刚才移除的蛇尾坐标加回
坐标处理与克隆:
- 在初始化蛇身时,我们使用了
pos.clone()。这是因为传入的snakeInit是外部数组,直接引用可能导致外部代码意外修改游戏内部状态。进行防御性拷贝是一个好习惯。 - 在计算新头时,我们创建了新的
int[]数组,而不是修改原蛇头数组,保持了数据的不可变性。
- 在初始化蛇身时,我们使用了
4. 测试用例设计与边界条件验证
写完代码不代表工作结束,设计全面的测试用例进行验证至关重要。机考环境通常也会提供多个测试用例来验证你的程序。
4.1 常规功能测试
public static void main(String[] args) { // 测试用例1:简单移动,不吃食物 // 场地3x3,蛇初始在[(2,0), (1,0), (0,0)],食物在[(0,2)],指令"R R" // 蛇向右移动两格,最终长度应为3 int[][] snake1 = {{2,0}, {1,0}, {0,0}}; int[][] food1 = {{0,2}}; String cmd1 = "RR"; int result1 = playGame(3, 3, snake1, food1, cmd1); System.out.println("Test 1 - Simple Move: " + result1 + " (Expected: 3)"); // 测试用例2:吃到食物 // 场地3x3,蛇初始在[(0,0)],食物在[(0,1), (1,1)],指令"R D" // 蛇右移吃到(0,1),长度变2;下移吃到(1,1),长度变3 int[][] snake2 = {{0,0}}; int[][] food2 = {{0,1}, {1,1}}; String cmd2 = "RD"; int result2 = playGame(3, 3, snake2, food2, cmd2); System.out.println("Test 2 - Eat Food: " + result2 + " (Expected: 3)"); // 测试用例3:撞墙死亡 // 场地2x2,蛇在[(0,0)],食物无,指令"U" // 向上移动出界,应返回-1 int[][] snake3 = {{0,0}}; int[][] food3 = {}; String cmd3 = "U"; int result3 = playGame(2, 2, snake3, food3, cmd3); System.out.println("Test 3 - Hit Wall: " + result3 + " (Expected: -1)"); // 测试用例4:撞到自己死亡 // 场地3x3,蛇在[(1,1), (1,0), (0,0)] (L形),食物无,指令"U L" // 先向上到(0,1),再向左到(0,0),但(0,0)是身体一部分(原蛇尾),但此时原蛇尾(0,0)是否已移开? // 我们需要一个更典型的撞身案例:蛇身形成一个圈,头朝圈内移动。 // 简化:蛇在[(0,0), (0,1), (1,1), (1,0)] (2x2方块),头在(0,0),指令"R" // 向右移动到(0,1),但(0,1)是身体,应死亡。 int[][] snake4 = {{0,0}, {0,1}, {1,1}, {1,0}}; // 这是一个环,头尾不相邻?这里头是(0,0) // 实际上这个环的移动很复杂。我们用一个简单的:蛇身[(0,0), (0,1), (0,2)],头在(0,0),指令"R R L" // 右移到(0,1)是身体吗?不,(0,1)是身体,但此时蛇尾是(0,2),所以(0,1)还在身体里。对,会撞到。 // 让我们重新设计一个清晰的用例。 }4.2 边界与陷阱测试
// 测试用例5:移动后头与旧尾重合(合法情况) // 场地3x3,蛇初始为一直线[(2,0), (1,0), (0,0)],食物无,指令"U" // 蛇头(2,0)上移到(1,0),而(1,0)是当前身体的第二部分。但这是向前移动,新头位置是旧的身体第二节,不是旧尾。 // 我们需要一个头移动到旧尾的案例:蛇身[(1,0), (0,0)],头在(1,0),尾在(0,0)。指令"L"。 // 头左移到(1,-1)出界了。不对。 // 正确的案例:蛇身[(1,1), (1,0), (0,0)],头在(1,1)。指令"L U L"。 // 第一步L: (1,1)->(1,0) (撞到身体?(1,0)是身体,但它是蛇尾吗?当前蛇尾是(0,0),所以(1,0)不是蛇尾,是身体,应该撞死。) // 看来“头移动到旧尾”在直线移动中不会发生,因为尾会离开。只有在蛇打结时才会。 // 这个边界条件我们的“移除蛇尾检测法”已经处理。我们测试一个不会死亡的移动。 int[][] snake5 = {{1,1}, {1,2}, {0,2}, {0,1}}; // 一个顺时针小圈,头在(1,1) // 身体集合: (1,1), (1,2), (0,2), (0,1) // 指令"L":头左移到(1,0)。这不是身体,合法。 // 指令"U":头上移到(0,1)。(0,1)是身体,但它是当前蛇尾吗?当前蛇尾是(0,1)吗?我们看看顺序:队列头是(1,1),尾是(0,1)。是的,(0,1)是蛇尾。 // 移动前,我们先从bodySet移除蛇尾(0,1)。现在bodySet是{(1,1), (1,2), (0,2)}。 // 新头(0,1)不在bodySet中,所以判定为不碰撞。合法!这正是我们想要的效果。 int[][] food5 = {}; String cmd5 = "U"; int result5 = playGame(3, 3, snake5, food5, cmd5); System.out.println("Test 5 - Move to old tail (legal): " + result5 + " (Should not be -1, length 4)"); // 测试用例6:食物被吃光后继续移动 int[][] snake6 = {{0,0}}; int[][] food6 = {{0,1}}; String cmd6 = "R L R"; // 右移吃食物,左移,右移(此时无食物) int result6 = playGame(3, 3, snake6, food6, cmd6); System.out.println("Test 6 - Move after food exhausted: " + result6 + " (Expected: 2, and no error)"); // 测试用例7:空指令 String cmd7 = ""; int result7 = playGame(3, 3, snake1, food1, cmd7); System.out.println("Test 7 - Empty commands: " + result7 + " (Expected initial length: 3)");4.3 测试心得与常见错误
- 务必测试“头移动至旧尾”的情况:这是最容易出错的地方。我们的算法通过了测试5,证明了其正确性。
- 测试食物序列索引越界:当
foodIndex >= food.length时,意味着所有食物已吃完,后续移动不应再尝试吃食物。我们的代码中if (foodIndex < food.length && ...)判断确保了这一点。 - 初始蛇身长度可能大于1:不要假设蛇总是从长度为1开始。我们的初始化逻辑支持任意长度的初始蛇身。
- 指令字符串可能包含非法字符:题目通常保证输入合法,但健壮的程序可以考虑在
Direction.fromChar中处理非法输入,例如抛出异常或返回一个默认值。在机考中,如果题目没说,可以假设输入合法。
5. 性能优化与代码风格探讨
对于机考题目,在保证正确性的前提下,代码的清晰度和可读性有时比极致的性能微优化更重要。但了解优化方向是有益的。
5.1 时间复杂度与空间复杂度分析
- 时间复杂度:假设移动指令数为
K,蛇的最大长度为L。- 每次
move操作中,Deque的插入删除是O(1)。 HashSet的查找和插入平均也是O(1)。- 因此,总时间复杂度为
O(K),非常高效。
- 每次
- 空间复杂度:主要消耗在存储蛇身坐标的
Deque和HashSet上,为O(L)。食物序列存储为O(F)。总体是O(L + F)。
5.2 可能的优化点
- 坐标表示优化:我们使用
String如"x,y"作为HashSet的键。虽然方便,但创建字符串有微小开销。可以自定义一个Pair类或直接使用java.awt.Point(如果环境允许),并确保正确重写hashCode。更极致的优化是使用BitSet或二维布尔数组来标记地图位置,空间换时间,但可能不灵活。 - 方向映射优化:
Direction.fromChar用了循环查找。如果指令集只有4个,可以用switch语句或预先准备好的Map<Character, Direction>,但差别微乎其微。 - 减少对象创建:在
move方法中,每次都会创建新的int[]作为新头。在性能敏感的场合,可以考虑复用对象池,但对于机考和大多数应用,这没有必要。
5.3 代码风格与可维护性建议
- 命名清晰:像
bodySet、foodIndex这样的变量名,一眼就能看懂其用途。 - 方法单一职责:
Game.move方法虽然稍长,但逻辑步骤清晰(计算新头、撞墙、撞身、吃食物、更新状态)。也可以考虑拆分成isWallHit,isBodyHit,eatFood等私有方法,让move更像一个流程控制器。 - 使用枚举:用
Direction枚举代替魔数(如0,1,2,3)或字符直接比较,大大提升了代码的可读性和可维护性。 - 防御性编程:在构造函数中对输入数组进行克隆,避免了潜在的副作用。
- 注释关键算法:对于“移除蛇尾检测”这样的技巧,添加简要注释,方便日后自己或他人理解。
6. 常见“踩坑点”与实战心得
根据多年刷题和带新人的经验,实现贪吃蛇模拟题时,以下几个坑几乎每个人都会遇到至少一个:
- 撞身判断忽略蛇尾:这是最大的坑。很多人直接用新头坐标去和整个蛇身队列比较,忽略了移动后蛇尾会离开。导致蛇无法正常直线移动(因为新头总会和即将离开的旧尾重合)。务必使用“先移除蛇尾再判断”或“判断时排除蛇尾”的技巧。
- 食物吃完后的处理:当食物序列被吃光后,
foodIndex会等于food.length。后续移动中,判断是否吃到食物时,必须先检查foodIndex < food.length,否则会数组越界。我们的代码通过短路与&&确保了这一点。 - 坐标顺序混淆:题目中通常用
(x, y)表示坐标,但有时x是行,y是列;有时又反过来。一定要根据样例输入输出确认清楚。我们的代码中,rows对应x的范围[0, rows-1],cols对应y的范围[0, cols-1]。 - 初始蛇身顺序:蛇身序列给出的顺序是头到尾,还是尾到头?这决定了你是在队列头部还是尾部插入新头。通常,序列的第一个元素是蛇头。我们的实现假设传入的
snakeInit数组第一个坐标是头,依次到尾巴,因此初始化时按顺序addLast,这样队列的头部 (peekFirst) 就是蛇头。 - 死亡判定与返回值:题目要求中途死亡时返回什么?是返回
-1,还是返回死亡前的长度?一定要仔细阅读输出说明。我们的playGame函数在move返回false时返回-1,这是一种常见约定。 - 边界值测试:
- 场地为
1x1,蛇初始就在里面,任何移动都会撞墙。 - 蛇初始长度等于场地大小(
M*N),那么第一次移动必然撞墙或撞身(如果还有空间的话)。 - 移动指令字符串为空,应直接返回初始长度。
- 场地为
最后,在华为OD机考或类似限时编程环境中,建议按照以下步骤进行:
- 花5分钟仔细读题,用笔标记出所有实体、属性、规则和边界条件。
- 花5-10分钟设计,在草稿上画出数据结构,写出核心算法伪代码,特别是状态转移和死亡判断逻辑。
- 20-25分钟编码,按照设计实现,边写边思考边界。
- 最后5-10分钟测试,用题目给的样例和自己在草稿上设计的几个极端用例(空、满、撞尾、吃光食物等)进行测试。
这道“贪吃蛇”题目,掌握其核心状态管理和边界处理技巧后,你会发现它是一类问题的代表。无论是电梯调度、进程管理还是游戏AI,其内核都是对一个有状态系统进行精确的模拟。把这部分逻辑练扎实了,再遇到类似的“模拟题”,你就能游刃有余。