吃豆人搜索算法实战:状态空间、DFS/BFS/UCS/A*与启发式设计
简介UC Berkeley的AI Pacman项目是经典课程CS188配套实战项目这一实现聚焦Search部分的完整Python解决方案面向正在学习搜索算法、备考或完成课程作业的学生与开发者。内容浓缩了项目中最核心的搜索与决策环节既覆盖BFS、DFS、A*、Dijkstra等基础搜索算法也深入实现Minimax、α-β剪枝等博弈策略并附eightpuzzle、solveEightQueens等扩展任务便于巩固不同场景下的算法应用。压缩包共23个文件以20个Python源码文件为主体涵盖pacman.py游戏环境、search.py核心算法、searchAgents.py搜索智能体、graphicsDisplay.py可视化及autograder.py自动评测脚本另附README.md、commands.txt和LICENSE说明整体仅67KB目录结构清晰便于学习。目前已有561人学习/下载适合需要阅读完整实现、调试搜索逻辑或参考作业写法的人群可结合实际运行加深对AI搜索与博弈算法的理解整体思路也可作为算法课设或面试复习的参考。1. 从一道 AI 课程作业看透搜索算法Berkeley CS188 的 Pacman Search 到底考什么在 AI 入门阶段UC Berkeley CS188 的 AI-Pacman-Project_Search 是一个绕不开的项目。第一阶段叫 Search但你不需要给吃豆人Pacman写游戏逻辑你要做的是用 DFS、BFS、UCS、A* 这些搜索算法让吃豆人自己走迷宫、去最近的食物、走遍四个角落甚至吃光整张地图上的豆子。它和刷题最大的区别在于你要先把走迷宫的过程抽象成状态空间搜索——状态怎么定义、后继节点怎么生成、启发式函数怎么设计每一步都对应真实 AI Agent 的决策链路。这篇文章面向刚接触 AI 编程的学习者、准备面试的求职者以及想把路径规划真正落地成代码的人。我会按“问题抽象 → 算法实现 → 启发式设计 → 排错”的顺序把整个方案的边界和坑都讲清楚。2. 把迷宫寻路重构成标准搜索问题State、Action 与 SearchProblem 接口2.1 为什么搜索是 AI Agent 决策的通用框架Pacman 项目的课程设计里有个关键词叫 agent。吃豆人就是一个 AI agent它每步要决策往东、往西、往南还是往北决策依据是当前看到的迷宫状态。自动驾驶的局部路径规划、仓储机器人寻路、游戏 NPC 追踪目标本质上都是同一个框架给定当前状态枚举动作预测下一步状态找到一条能到达目标的路径。搜索算法解决的就是这个“怎么找路径”的问题。Berkeley 把迷宫行走设计成搜索问题是刻意要你去掉游戏感用状态空间的视角看问题。状态不再是一张地图而是“吃豆人当前在哪个格子”动作不再是动画反馈而是四个方向的一次移动目标也不再是“吃完食物”而是“是否抵达某个格子”或“四个角落是否都已访问”。一旦你这么抽象DFS、BFS、UCS、A* 就全部和迷宫无关了它们只认三个接口起点、目标判断、后继展开。我见过不少同学卡在这道题上不是因为算法难而是不愿意把游戏世界拆成状态和动作。反过来只要抽象做对了搜索算法的代码量很小甚至可以说是同一模板改三行。所以这一章先把 SearchProblem 接口讲透这是整个项目的地基。2.2 SearchProblem 的四个方法先定接口再写算法课程作业公开的框架里search.py 定义了一个 SearchProblem 抽象类实现它的类需要补齐四个方法。这就是搜索算法和具体问题之间的契约。class SearchProblem: def getStartState(self): 返回搜索起点状态。Pacman 项目里通常是 (x, y) 坐标 复杂问题里会是一个包含位置和额外信息的元组。 raise NotImplementedError def isGoalState(self, state): 判断当前状态是否是目标状态。 吃豆人项目里常见判断是当前位置是否为食物格。 raise NotImplementedError def getSuccessors(self, state): 返回 [(nextState, action, stepCost), ...]。 nextState 是转移后的状态action 是动作名 stepCost 是从当前状态走到 nextState 的单步代价。 raise NotImplementedError def getCostOfActions(self, actions): 给定一个动作序列累加总代价。 这项不参与搜索算法但自动评测器会拿它验证你的解是否最优。 raise NotImplementedError逻辑说明getStartState 给搜索算法一个初始状态isGoalState 在搜索循环里被反复调用一旦返回 True算法就停止并返回路径getSuccessors 是状态空间展开的唯一入口搜索算法的全部“世界知识”都来自它。getCostOfActions 不参与搜索但 autograder 会用它计算你提交的动作序列的代价判断你是否真的找到了最短路径。参数说明项目约定迷宫坐标用 (x, y) 表示x 是列索引y 是行索引而且 y 从下往上增长。这个约定和二维数组常用的 (row, col) 不一样很多人在这里翻车把 y 当行号用结果 north 和 south 方向互换搜索出来的路径在视觉上完全反了。我一般会在 getSuccessors 里先打印一次四个方向的目标坐标确认坐标系方向没有反。2.3 最小落地的 SingleFoodSearchProblem 代码与参数说明在真正写 SearchAgent 之前我建议先实现一个只找一块食物的最小问题。它足够简单能让你最快验证搜索算法本身有没有写对。常见做法是继承 SearchProblem构造时传入起点、食物坐标和墙壁矩阵。class SingleFoodSearchProblem(SearchProblem): 只找一块食物的最小搜索问题用来快速验证 DFS/BFS/UCS/A*。 def __init__(self, start, food_pos, walls): # start: (x, y) 起点坐标 # food_pos: (x, y) 食物坐标 # walls: 二维布尔矩阵True 表示墙False 表示可走 self.start start self.food food_pos self.walls walls def getStartState(self): return self.start def isGoalState(self, state): return state self.food def getSuccessors(self, state): x, y state succ [] # 四个方向的移动量和动作名顺序会影响 DFS 找到的路径 for dx, dy, action in [(1, 0, East), (-1, 0, West), (0, 1, North), (0, -1, South)]: nx, ny x dx, y dy # 越界或撞墙都跳过 if 0 nx self.walls.width and 0 ny self.walls.height: if not self.walls[nx][ny]: succ.append(((nx, ny), action, 1)) return succ def getCostOfActions(self, actions): return len(actions)逻辑说明getSuccessors 里按 East、West、North、South 的固定顺序枚举动作。这个顺序很关键DFS 会优先尝试 East所以同样的迷宫先 East 后 West 和先 North 后 South 找到的路径可能完全不同虽然长度不一定变。每次走到一个新状态单步代价是 1所以动作序列的长度就是总代价。参数说明walls 矩阵的索引方式要小心self.walls[nx][ny] 的第一个索引是 x列第二个是 y行和很多二维数组先 row 后 col 的习惯相反。越界判断也不能少否则搜索算法可能在边界格子产生非法的后继状态导致路径穿过墙或者走出地图。3. 手写 DFS、BFS、UCS、A* 四个搜索函数代码与运行对比3.1 DFS 与 BFS一个用栈一个用队列在 Pacman 的 Search 项目里第一关和第二关分别要求实现深度优先搜索DFS和广度优先搜索BFS。两者的差别只有一个容器DFS 用栈后进先出一条路走到黑再回头BFS 用队列先进先出一层一层往外扩。这个概念在课本上很好懂但写进代码时visited 集合的更新时机才是不分新手老手都会踩的坑。from util import Stack def depthFirstSearch(problem): 深度优先搜索栈 visited 集合。 closed set() # 已扩展状态集合 start problem.getStartState() stack Stack() # 栈里保存 (状态, 从起点到该状态的动作序列) stack.push((start, [])) while not stack.isEmpty(): state, path stack.pop() if problem.isGoalState(state): return path # 注意DFS 不保证最短路径 if state in closed: continue closed.add(state) # 第一次弹出时才加入 closed for nextState, action, stepCost in problem.getSuccessors(state): # 不走回头路如果后继已经在 closed 里跳过 if nextState not in closed: stack.push((nextState, path [action])) return []逻辑说明这段代码是典型的图搜索 DFS。每次从栈里弹出一个状态先判断是不是目标再判断是否在 closed 里。我习惯在“弹出后”才加入 closed而不是在“压栈时”加入原因会在第 5 章细讲这里先记住被扩展过的状态才进 closed只被压栈的状态不进。path [action] 会生成新的列表每个分支持有自己的路径互不干扰。参数说明Stack 容器来自 util.py是课程框架自带的也可以用 Python 的 list 模拟append 入栈、pop 出栈。DFS 的时间复杂度在最坏情况下是 O(b^m)b 是分支因子迷宫里是 4m 是最大深度空间复杂度只有 O(bm)所以它对内存很友好但路径不一定短。BFS 几乎是一模一样的代码只需要把 Stack 换成 Queue再把 append/pop 的语义换掉from util import Queue def breadthFirstSearch(problem): 广度优先搜索队列 visited 集合保证第一个目标是全局最短。 closed set() start problem.getStartState() queue Queue() queue.push((start, [])) while not queue.isEmpty(): state, path queue.pop() if problem.isGoalState(state): return path # 首次找到目标即最短路径 if state in closed: continue closed.add(state) for nextState, action, stepCost in problem.getSuccessors(state): queue.push((nextState, path [action])) return []逻辑说明BFS 保证在边权为 1 的图上第一次遇到目标时路径最短。原因是队列按层弹出第 k 层状态一定在第 k1 层之前被处理。Pacman 迷宫里的单步代价全是 1所以 BFS 能给出最短路径。它的缺点也很明显内存消耗是 O(b^d)d 是解的深度bigMaze 里状态一多内存上涨很快但课程规模完全撑得住。参数说明closed 集合的作用是防止重复扩展。如果迷宫里有环路不用 closed 就会无限循环就算没有环路浪费性能在重复状态上也会让搜索慢到没法看。这里 BFS 弹出一个状态后先判断目标再加入 closed是标准图搜索写法不要在前面用“压栈时加入”的手法。3.2 UCS用优先级队列按代价扩展第三关要实现的是一致代价搜索UCS。它和 BFS 唯一的区别是BFS 按层扩展UCS 按累计代价扩展。迷宫默认每步代价为 1UCS 和 BFS 的行为几乎一样但课程会构造带可变代价的格子比如某些区域移动代价特别高这时你就得用优先级队列PriorityQueue按累计代价最小的节点优先弹出。from util import PriorityQueue def uniformCostSearch(problem): 一致代价搜索按累计代价从小到大扩展保证最短代价路径。 closed {} # state - 已知最小到达代价 start problem.getStartState() pq PriorityQueue() # 优先级队列存 (state, path, cost)按 cost 排序 pq.push((start, [], 0), 0) while not pq.isEmpty(): state, path, cost pq.pop() if problem.isGoalState(state): return path # 如果这个状态之前用更低代价到达过跳过当前节点 if state in closed and closed[state] cost: continue closed[state] cost for nextState, action, stepCost in problem.getSuccessors(state): new_cost cost stepCost pq.push((nextState, path [action], new_cost), new_cost) return []逻辑说明UCS 的本质是 Dijkstra 算法的“到达目标即停止”版本。closed 不再是一个普通集合而是一张记录状态最小代价的表。这里我用 closed[state] cost 做判断意味着如果同一个状态被更便宜的路径再次到达还会被重新扩展如果到达代价更大或相等直接丢弃。这个细节决定了 UCS 在最坏情况下还能不能保证最优。参数说明PriorityQueue 的 push 方法第二个参数是优先级越小越先弹出。课程框架里的 PriorityQueue 支持带计数的 push相同优先级时先进入的先弹出避免出现因为排序不稳定导致的行为差异。如果你自己实现建议用 heapq元组里加一个自增序号做平局打破。3.3 A*启发式函数决定搜索效率第四关的 A* 搜索是 UCS 加上启发式函数。f g hg 是已经走过的实际代价h 是对未来还要花多少代价的估计。h 越接近真实值搜索越高效h 越好扩展节点越少。很多人的第一反应是 h 越大越好其实错了一旦 h 过高A* 就变成贪心最优性直接丢。from util import PriorityQueue def nullHeuristic(state, problemNone): 空启发式返回 0A* 退化成 UCS。 return 0 def aStarSearch(problem, heuristicnullHeuristic): A* 搜索f g hh 来自启发式函数。 closed set() start problem.getStartState() pq PriorityQueue() pq.push((start, [], 0), heuristic(start, problem)) while not pq.isEmpty(): state, path, cost pq.pop() if problem.isGoalState(state): return path if state in closed: continue closed.add(state) for nextState, action, stepCost in problem.getSuccessors(state): new_cost cost stepCost priority new_cost heuristic(nextState, problem) pq.push((nextState, path [action], new_cost), priority) return []逻辑说明A* 的代码结构和 UCS 几乎一样只是优先级从 cost 变成了 cost heuristic。启发式函数接收当前状态和问题对象返回未来代价估计值。nullHeuristic 恒为 0A* 就退化成 UCS。课程里默认把 nullHeuristic 当成回调传进来这是 Python 里非常典型的策略模式用法你换一个启发式函数不需要动搜索主循环。参数说明A* 保证最优的前提是启发式函数可采纳且有界。可采纳意味着 h 永远不会超过真实剩余代价一致意味着 h 满足三角不等式。作业里常见的内置启发式是曼哈顿距离因为迷宫只允许上下左右移动曼哈顿距离恰好是真实距离的下界直接可采纳。自己写启发式时最优的做法是先证明可采纳性再谈效率。4. 从单目标到全覆盖搜索CornersProblem 与 FoodSearchProblem 的启发式设计4.1 CornersProblem把“走遍四个角”写进状态第五关是经典的 Corners Problem。目标不再是到达某个格子而是吃豆人要“访问过迷宫的四个角落”。这时状态就不只是坐标了你得把“哪些角落已经访问过”也编进状态里否则算法根本没有办法区分“还没去左下角”和“已经去过左下角”的两种情况。class CornersProblem(SearchProblem): def __init__(self, startingGameState): self.walls startingGameState.getWalls() # 迷宫的地图和墙信息边走边知道当前位置 top self.walls.height - 2 right self.walls.width - 2 self.corners ((1, 1), (1, top), (right, 1), (right, top)) self.startingPosition startingGameState.getPacmanPosition() # 状态 (当前位置, 四个角落是否访问过的布尔元组) self.visited_corners (False, False, False, False) def getStartState(self): return (self.startingPosition, self.visited_corners) def isGoalState(self, state): position, visited state return all(visited) # 四个角落都访问过才叫目标 def getSuccessors(self, state): position, visited state x, y position succ [] for dx, dy, action in [(1, 0, East), (-1, 0, West), (0, 1, North), (0, -1, South)]: nx, ny x dx, y dy if not self.walls[nx][ny]: # 关键走进角落时要把对应的布尔位改成 True new_visited list(visited) if (nx, ny) in self.corners: idx self.corners.index((nx, ny)) new_visited[idx] True next_state ((nx, ny), tuple(new_visited)) succ.append((next_state, action, 1)) return succ逻辑说明状态从“一个坐标”扩展成“坐标 已访问角落的位图”。多个不同的状态可能落在同一个坐标上但访问历史不同这对搜索算法是两回事。isGoalState 的判断从“坐标相等”改成“访问位图全为 True”这一步是覆盖类搜索的核心思想目标测试不再只看当前位置。参数说明self.corners 是死角落的坐标我用 1 而不用 0是因为课程迷宫的外墙通常占一圈合法坐标从 1 开始。如果你用 0 去判断角落很可能把墙也算成角落。访问位图我用 tuple 不用 list是防止后继状态之间互相篡改在最外层用 list(visited) 复制一份再修改保证每个 branch 都有自己的副本。4.2 cornersHeuristic先做可接受的再做紧的Corners Problem 状态一多BFS 就非常吃力所以第六关要做的是给 A* 写一个角落启发式。第一版最容易想到的启发式是当前状态到最近未访问角落的曼哈顿距离。这个启发式可采纳但太弱搜索时扩展的节点特别多想要有效率得把四个角落看成是一组“必须全部覆盖的航点”。def cornersHeuristic(state, problem): 到最近未访问角落的曼哈顿距离。 这是可采纳下界但不是最优启发式。 position, visited state unvisited [] for idx, corner in enumerate(problem.corners): if not visited[idx]: unvisited.append(corner) if not unvisited: return 0 # 下界当前位置到任意未访问角落的最短曼哈顿距离 return min(abs(position[0] - cx) abs(position[1] - cy) for cx, cy in unvisited)逻辑说明可采纳性的证明很简单——无论下一步怎么走要继续访问完所有未访问角落最快的路径也必须先从当前位置到其中某一个角落这段距离的真实代价下界就是曼哈顿距离。用 min 这个写法是安全的因为实际成本必然大于等于这个最小值。参数说明曼哈顿距离假设没有墙真实迷宫会有墙所以真实代价只可能更大这也正是它为什么可以作为可采纳下界。如果你在其实有障碍的地图上用了欧氏距离同样可采纳但更弱搜索扩展数量会明显上升。想更强一点的启发式可以对未访问角落集合做最小生成树MST把 MST 总长加进启发式只要你不超过真实最短巡游路径A* 就仍然最优。4.3 foodHeuristic从最近食物到最小生成树第七关要求吃光所有豆子Eating All the Dots。这是整个 Search 部分最难的关卡因为它本质上是个旅行商问题。状态里除了吃豆人坐标还得有一张食物网格标记还有哪些豆子没吃。这时候启发式函数的质量直接决定你要不要等上十分钟。def foodHeuristic(state, problem): 吃光所有食物的启发式 返回值 到最近食物的曼哈顿距离。 这是可采纳的但特别弱再强的版本会把剩余食物的 MST 加进来。 position, foodGrid state foods [] # 扫描食物网格收集所有还未被吃掉的豆子坐标 for x in range(foodGrid.width): for y in range(foodGrid.height): if foodGrid[x][y]: foods.append((x, y)) if not foods: return 0 return min(abs(position[0] - fx) abs(position[1] - fy) for fx, fy in foods)逻辑说明foodHeuristic 的输入 state 里foodGrid 是一个布尔网格True 表示这个格子上还有豆子。我要做的是把豆子坐标抽出来然后算当前位置到最近豆子的曼哈顿距离。它的直观解释是不管接下来怎么吃至少要走到某个豆子面前所以真实剩余代价一定大于等于这个最小距离。参数说明这个启发式虽然可采纳但距离真实代价太远在食物密集的地图上扩展节点数非常多。常见做法是做一个“两阶段启发式”先算优当前点到每个食物的距离再算所有食物两两之间的曼哈顿距离用最小生成树MST近似覆盖全部食物的最短路径然后把 MST 总代价作为启发式的一部分。原因在于吃掉所有食物的最短路不可能小于“访问所有食物位置”的最短连通成本。更进阶的做法是用到“最远食物距离”做下界当前位置到所有剩余食物中距离最远的那个的曼哈顿距离也是真实成本的下界因为最后至少要走那么远。你可以把多个可采纳启发式取最大值得到更紧的下界而且仍然可采纳这是课程里被广泛使用但很少被讲透的技巧。5. 避坑吃豆人搜索项目里最常翻车的 5 个问题5.1 诡异路径DFS/BFS 返回的路径不是最短或者搜索时间异常现象BFS 明明应该返回最短路径但跑 smallMaze 时返回的路径明显绕圈或者 DFS 在同一个迷宫上反复扩展同一个状态。原因closed 集合的更新时机错了。很多人在压栈时就顺手把 nextState 加入 closed导致一个状态第一次被发现就被判为“已扩展”于是 BFS 丢失了通过其他更短路径到达同一状态的可能性。在带环迷宫上压栈前入 closed 甚至会漏掉合法的、更短的重新访问路径。解决标准做法是弹出时才检查和加入 closed。我把这个逻辑写进了上面的 DFS/BFS/UCS/A* 四个函数里确保每次扩展的都是真正的“第一次被弹出的状态”。如果你的代码已经压栈前加入了 closed改起来很简单把 closed.add 移到 pop 之后压栈前只做 nextState not in closed 的判断。5.2 A* 慢过 BFS启发式不一致或太弱现象用曼哈顿距离做启发式的 A* 在 mediumMaze 上扩展节点比 BFS 还多运行时间肉眼可见地变慢。原因一是启发式太弱h 几乎等于 0A* 退化成 UCS而 UCS 在边权为 1 的图上等价于 BFS 但多一层优先队列开销二是启发式不一致f 值在父子节点之间不单调递减A* 会对同一个状态反复入队。解决先用 nullHeuristic 跑一遍确认 A* 和 UCS 行为一致再换曼哈顿距离观察扩展节点数有没有下降。如果还是慢就用多个可采纳启发式取最大值比如把“最近食物距离”“最远食物距离”“食物 MST 估计”三者取 max这样既保持可采纳性又能明显减少扩展节点。5.3 visitedCorners 一直是 (False, False, False, False)现象Corners Problem 跑完打印 visited 位图四个布尔值全是 FalseA* 永远找不到目标或者路径明明穿过了角落但 isGoalState 始终不满足。原因getSuccessors 里忘了更新角落位。常见翻车点是把 state 拆成 position, visited 后在循环里直接用了原始的 visited 而不是 new_visited导致每条后继路径都带着“未访问任何角落”的历史。解决每一轮生成后继都要基于当前状态的 visited 复制出新副本。我给出一小段可靠的写法new_visited list(visited) # 复制成可变列表 if (nx, ny) in problem.corners: new_visited[problem.corners.index((nx, ny))] True next_state ((nx, ny), tuple(new_visited))注意 index() 每次查找是 O(4)角落只有四个性能完全够但如果你的状态里角落数量上升建议直接建立坐标到索引的字典。调试时我在 isGoalState 里加一行 print(state)看看有没有一个状态能凑出全 True一般几秒钟就能定位问题根源。5.4 RecursionError递归 DFS 撞上 Python 默认递归上限现象用递归方式写 DFS跑 bigMaze 时报 RecursionError: maximum recursion depth exceeded。原因Python 默认递归上限大约 1000 层。迷宫深度一旦超过 1000递归调用栈直接炸掉。DFS 适合用显式栈而不是递归。解决把递归 DFS 改成迭代式用 util.Stack 里的显式栈来管理状态和路径。如果你实在想保留递归写法可以在文件顶部加 sys.setrecursionlimit(10000)但这是治标不治本深层递归不仅容易炸栈调试回溯信息也很难读。我自己的习惯是搜索类项目一律用迭代式栈和队列是同一个 push/pop 语义区别只在容器是 Stack 还是 Queue。5.5 超时FoodSearchProblem 不能当成单目标问题硬搜现象用 BFS 或 UCS 跑食物全收集地图稍大就超时甚至几分钟都出不来结果。原因把“吃光所有豆子”当成普通迷宫寻路去搜状态空间会爆炸。食物有几十颗豆子每个状态里都要带一张食物网格BFS 扩展的节点数量和状态组合数成正比直接量级翻番。解决用 A* 加上启发式函数。不要把 foodHeuristic 当作可有可无的优化项它是整个项目的胜负手。课程评测对时间有硬性指标你自己做方案时也应该把启发式当成第一优先级先保证可采纳再逐步加紧。实测下来用“到最近食物距离”能过小型图“最远食物距离 MST 近似”才能稳过 trickySearch 级别的图。6. 让搜索过程可视化并验证结果调试与验收技巧课程框架自带 pacman.py 命令行入口调试时不需要写额外脚本。常规运行方式是这样的python pacman.py -l tinyMaze -p SearchAgent -a fndfs python pacman.py -l mediumMaze -p SearchAgent -a fnbfs python pacman.py -l bigMaze -p SearchAgent -a fnastar,heuristicmanhattanHeuristic python pacman.py -l smallCorners -p AStarCornersAgent -a fnastar,heuristiccornersHeuristic python pacman.py -l trickySearch -p AStarFoodSearchAgent运行效果窗口里吃豆人会按你写的算法一步一步移动路径越短说明算法越优。如果肉眼看不出来就加 --frameTime 0.01 放大帧间隔再看搜索动画里被访问过的格子数量expansion 越少说明启发式越紧。我最常用的一套验证方法是先跑 tinyMaze 和 smallMaze 确认行为正确再跑 mediumMaze 对比 DFS 与 BFS 的路径长度。如果两者长度一样说明这条迷宫地图给 DFS 也算碰巧找到最短如果不一样BFS 必然更短。接着跑 trickySearch这通常是考验启发式质量的分水岭——能短时间出结果说明你的启发式不是摆设。关于自动评测我会在本地跑一次 autograder 的搜索部分确认每个 query 的路径代价和扩展节点数都在合理范围。值得注意的是有的同学会为了测评通过去给启发式函数加特判我不建议这么干因为启发式一旦不可采纳A* 的路径质量波动很大换一张地图就翻车。老老实实从下界出发去构造启发式后续做多智能体搜索时你会感谢自己当初的克制。这个项目越往后越明显的一个规律是搜索算法本身只占一小部分真正费时间的是状态设计和启发式设计。我把这个习惯带到了后来的很多路径规划方案里每次动手写代码前先把状态和代价模型写在纸上。希望帮到你。本文还有配套的精品资源点击获取