新闻详情

新闻详情

首页 / 资讯中心 / 详情

UC Berkeley Pacman 搜索项目:四大搜索算法与启发式实现

发布时间:2026/9/25 13:40:53来源:尧图网络
UC Berkeley Pacman 搜索项目:四大搜索算法与启发式实现
简介这是一份面向人工智能初学者的 UC Berkeley AI Pacman 项目搜索算法解决方案使用 Python 编写完整覆盖经典吃豆人游戏中的路径规划与智能决策问题尤其适合正在学习 CS188 或人工智能导论课程的学生。压缩包共 23 个文件20 个 Python 脚本构成核心代码涵盖搜索代理、图形显示、游戏逻辑、自动评分等模块另有 1 份 Markdown 文档、1 份命令文本及 1 份开源许可证整体仅 67KB轻量易部署。目前已有 561 人浏览学习实践反馈良好。代码中实现了 BFS、DFS、A*、Dijkstra 等经典搜索算法并进一步扩展至 Minimax、α-β 剪枝与 Q-learning 等进阶策略配套的 searchAgents.py、graphicsDisplay.py 等文件支持直接运行与可视化调试还附带八数码、八皇后等经典搜索问题的扩展实现。无论是完成课程作业、竞赛准备还是深入理解 AI 决策原理这份资源都能提供可参考的完整示例与实现思路。1. 这不是游戏代打UC Berkeley 的 Search 项目到底要你交什么先说结论这份 UC Berkeley AI Pacman 项目的 Search 解决方案是一整套 CS188 第一周作业的完整实现不是给游戏写外挂的脚本。它的核心是四个搜索算法——DFS、BFS、UCS、A*外加角落问题CornersProblem和食物问题FoodSearchProblem两套自定义建模与启发式函数全部落在 Python 写的官方框架里。你拿到手能直接在 tinyMaze、mediumMaze、bigMaze、trickySearch 这些官图上一行命令跑起来并用 autograder 对照 q1–q7 自查得分。适合三种人正在啃 AI 导论、对着模板发呆的留学生自学搜索算法想找个可视化 playground 的开发者以及想抄一套干净实现当 AI 应用开发面试底稿的求职者。2. 四个搜索算法一次写对栈、队列、优先队列与路径还原官方项目里需要你动笔的只有两个文件search.py 和 searchAgents.py。search.py 里四个空函数对应 autograder 的 q1–q4searchAgents.py 里的三个启发式对应 q5–q7。很多第一次做的人卡在同一个地方算法思路都懂但不知道 SearchProblem 这个抽象类到底给了什么返回值到底该是什么。这一章先把框架讲透再给能直接抄的四个函数最后给一套验证命令。2.1 SearchProblem 抽象类三个接口决定一切你写的每个搜索函数都只接收一个 problem 参数它长这样class SearchProblem: def getStartState(self): # 返回初始状态例如吃豆人的起始坐标 (x, y) ... def isGoalState(self, state): # 判断某个状态是否到达目标 ... def getSuccessors(self, state): # 返回 [(nextState, action, stepCost), ...] # action 是 North / South / East / West 这样的字符串 ...重点是返回值约定四个搜索函数最后都要返回一个动作列表比如[North, East, East]而不是状态坐标列表也不是总代价。为什么这么设计因为 SearchAgent 这个 AI Agent 每帧要决定往哪走pacman.py拿到动作列表后按顺序消费autograder 验证的也是「从起点按这些动作走能不能到目标」。另一个容易忽略的点getSuccessors 返回的 action 是字符串cost 是数值搜索节点里每一步是谁、走的什么方向都由这个三元组携带。我见过有人把 cost 丢了的版本写到 UCS 才发现要回头补所以一开始就在节点里带上 cost 最省事。2.2 DFS 和 BFS同一个模板换一种容器from util import Stack, Queue def depthFirstSearch(problem): frontier Stack() # LIFO后进先出 frontier.push((problem.getStartState(), [])) visited set() while not frontier.isEmpty(): state, actions frontier.pop() if problem.isGoalState(state): return actions # 返回动作序列 if state not in visited: visited.add(state) # 弹出时才标记 for nxt, action, stepCost in problem.getSuccessors(state): if nxt not in visited: frontier.push((nxt, actions [action])) return [] def breadthFirstSearch(problem): frontier Queue() # FIFO先进先出 frontier.push((problem.getStartState(), [])) visited set() while not frontier.isEmpty(): state, actions frontier.pop() if problem.isGoalState(state): return actions if state not in visited: visited.add(state) for nxt, action, stepCost in problem.getSuccessors(state): if nxt not in visited: frontier.push((nxt, actions [action])) return []两个函数结构完全一样唯一的差别是Stack()换成了Queue()这就是 DFS 与 BFS 的全部区别。逻辑说明每次从容器里弹出一个节点先判目标、再判 visited、然后展开后继。visited 统一在弹出时加入同时 push 前再做一次去重过滤——这是一套双保险push 前过滤防止重复状态在容器里堆积弹出时再确认是为了兜住「两个不同父节点把同一个子节点同时压入」的边界情况。参数说明actions [action]是新建列表而不是 append因为多条分支必须各自持有独立路径共享一个列表会互相污染。在 PositionSearchProblem 里所有 stepCost 都是 1所以同一张图上 UCS 和 BFS 表现几乎一致q3 真正考的是你在非等权图上怎么处理累计代价与弹出顺序。2.3 UCS 和 A*g 与 h 怎么进优先队列from util import PriorityQueue def uniformCostSearch(problem): frontier PriorityQueue() start problem.getStartState() frontier.push((start, [], 0), 0) visited {} # state - 目前为止的最好代价 while not frontier.isEmpty(): state, actions, cost frontier.pop() if problem.isGoalState(state): return actions if state in visited and visited[state] cost: continue # 这个状态已经以更优代价展开过 visited[state] cost for nxt, action, stepCost in problem.getSuccessors(state): newCost cost stepCost if nxt not in visited or newCost visited[nxt]: frontier.push((nxt, actions [action], newCost), newCost) return []逻辑说明优先队列按 priority 弹出最小元素UCS 的 priority 就是累计代价 g。这里故意没有用「标记-更新」式的闭包表因为 util 里的 PriorityQueue 只提供 push/pop没有 decrease-key常见做法是允许同一个状态重复进队列靠 visited 字典在弹出时裁掉更差的版本。A* 与 UCS 的差异只有一行def aStarSearch(problem, heuristicnullHeuristic): frontier PriorityQueue() start problem.getStartState() frontier.push((start, [], 0), heuristic(start, problem)) visited {} while not frontier.isEmpty(): state, actions, cost frontier.pop() if problem.isGoalState(state): return actions if state in visited and visited[state] cost: continue visited[state] cost for nxt, action, stepCost in problem.getSuccessors(state): newCost cost stepCost if nxt not in visited or newCost visited[nxt]: priority newCost heuristic(nxt, problem) frontier.push((nxt, actions [action], newCost), priority) return []A* 的 priority 变成newCost heuristic(nxt, problem)其中 newCost 是已经付出的 gh 是到目标的估计。search.py 顶部的nullHeuristic返回 0把它接进来A* 就自动回落到 UCS——这也是为什么 q3、q4 可以共用同一套验证链。2.4 命令行验证从 tinyMaze 到 openMaze模板自带的 pacman.py 是完整可运行的游戏框架验证搜索算法的命令格式是python pacman.py -l 地图名 -p SearchAgent -a fn算法名,heuristic启发式名参数说明参数作用常用值-l指定 layouts 目录下的 .lay 地图tinyMaze / mediumMaze / bigMaze / openMaze / trickySearch-p指定吃豆人 AgentSearchAgent-a传给 Agent 的参数fndfs, fnastar, heuristic..., prob...-q静默模式只打印路径代价与统计—--frameTime动画帧间隔0 为最快0 或更小数值一套标准验证命令按梯度跑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 openMaze -p SearchAgent -a fnucs对照关系很直观tinyMaze 上四个算法结果一样闭着眼都能过mediumMaze 用 BFS 拿到最短步数DFS 虽然也找到解但路径肉眼可见地绕bigMaze 没有曼哈顿启发式的裸 A* 会慢慢爬带heuristicmanhattanHeuristic通常一秒内出结果。跑的时候我建议一律加-q别让动画拖慢验证节奏真要开可视化把--frameTime调小默认值在复杂地图上非常劝退。3. 启发式函数决定成败曼哈顿、角落问题与食物问题的下界设计q5–q7 全部在 searchAgents.py 里完成q5 要一个 PositionSearchProblem 用的曼哈顿启发式q6 要 CornersProblem 的角落启发式q7 要 FoodSearchProblem 的食物启发式。前两个是白送分真正拉开差距的是第三个。这一章把三个启发式的下界思路拆开讲。3.1 可采纳性与一致性A* 最优性的两条命脉先把两个词钉死。可采纳性admissible要求 h(state) 永远不超过从 state 到目标的真实最短代价这是 A* 返回最优解的必要条件一致性consistency要求 h(A) ≤ c(A, B) h(B)也就是说沿边前进时启发式不能跳崖式下降它保证图搜索版本下每个节点最多被展开一次。我记这两个概念的方法可采纳性是「不许高估」一致性是「不许突然变乐观」。如果启发式高估A* 就会像生成式模型产生幻觉一样自信满满地朝一条次优路径冲过去最后返回一个非最优的动作序列autograder 直接判 fail。所以每次写完启发式先在小地图上验证路径代价是否等于 BFS 的基准值再谈展开节点数。3.2 cornersHeuristic四个角都要去下界怎么算CornersProblem 的状态是二元组(吃豆人位置, visitedCorners)visitedCorners 是四个布尔值表示四个角各去过没有目标是把四个角全部打卡。这里最容易写出不可采纳的启发式把到所有未访问角的曼哈顿距离直接求和。以吃豆人在 (5,5)、角在 (0,0)、(0,10)、(10,0)、(10,10) 为例四条距离加起来就是严重高估——真实路径是一条串行走过的折线不是从当前位置向四个角放射。def cornersHeuristic(state, problem): pos, visitedCorners state # visitedCorners 是 (bool, bool, bool, bool)下标对应 problem.corners unvisited [problem.corners[i] for i in range(4) if not visitedCorners[i]] if not unvisited: return 0 def md(a, b): return abs(a[0] - b[0]) abs(a[1] - b[1]) # 下界1当前位置到最近的未访问角 nearest min(md(pos, c) for c in unvisited) # 下界2未访问角之间的最小生成树Prim曼哈顿距离作边权 nodes unvisited[:] mst_cost 0 in_tree {nodes[0]} rest set(nodes[1:]) while rest: edge min((md(a, b), a, b) for a in in_tree for b in rest) mst_cost edge[0] in_tree.add(edge[1]) rest.remove(edge[1]) return nearest mst_cost逻辑说明任何可行路径都必然先到达第一个未访问角这段距离至少是 nearest之后要把剩余未访问角全串起来把所有未访问角连通的最小代价就是它们之间的最小生成树。所以 nearest MST 是真实最优代价的合法下界。参数说明为什么用曼哈顿距离做 MST 边权因为迷宫里的真实距离一定不少于曼哈顿距离用更小的边权算出来的 MST 只会更小下界依然成立而且不需要在启发式里反复跑 BFS。四个角的规模下Prim 的开销可以忽略。problem.corners是构造 CornersProblem 时传入的四个角坐标列表顺序必须和 visitedCorners 下标一一对应写反了启发式会变得不可采纳。3.3 foodHeuristic把 trickySearch 的节点数压进 700FoodSearchProblem 的状态是(吃豆人位置, foodGrid)foodGrid 是一个布尔网格用foodGrid.asList()直接拿到所有剩余食物的坐标。q7 的硬指标是用fnastar, probFoodSearchProblem, heuristicfoodHeuristic跑 trickySearch要返回最优解且展开节点数少于 700。写 nullHeuristic 的话 A* 退化成 UCS在这个地图上节点数会膨胀到几千直接丢分。def foodHeuristic(state, problem): position, foodGrid state food foodGrid.asList() if not food: return 0 def md(a, b): return abs(a[0] - b[0]) abs(a[1] - b[1]) # 下界1当前位置到最近食物的距离 nearest min(md(position, f) for f in food) # 下界2所有食物之间的最小生成树曼哈顿边权 nodes food[:] mst_cost 0 in_tree {nodes[0]} rest set(nodes[1:]) while rest: edge min((md(a, b), a, b) for a in in_tree for b in rest) mst_cost edge[0] in_tree.add(edge[1]) rest.remove(edge[1]) return nearest mst_cost逻辑说明和角落问题同一个套路——先到最近食物再用食物之间的 MST 覆盖剩余食物两个下界相加。代码也几乎是从 3.2 平移过来的唯一区别是节点集合从「未访问角落」换成「剩余食物」。参数说明Prim 每轮找当前树到剩余节点的最短边食物数量在 trickySearch 上是几十的量级单次启发式计算是 O(m²)m 为剩余食物数配合 700 节点的限制整体耗时完全可控。真实复现经验曼哈顿边权版本在 trickySearch 上通常能把展开节点数压进 700。如果你本地跑出来刚好卡线另一个常见做法是把曼哈顿换成「对每对食物和位置预计算 BFS 真实距离」再算 MST下界更紧节点数能再降一个量级代价是初始化时要跑几十次 BFS地图越大越划算。3.4 三个启发式的对比选择问题常用启发式下界构成典型效果PositionSearchProblemq5manhattanHeuristic到单一目标点的曼哈顿距离bigMaze 上节点数远低于裸 BFSCornersProblemq6cornersHeuristic最近角距离 角间 MSTmediumCorners 节点数降一个量级以上FoodSearchProblemq7foodHeuristic最近食物距离 食物间 MSTtrickySearch 可过 700 节点硬指标选择逻辑启发式越紧A* 展开越少但每次计算越贵。曼哈顿距离是 O(1)四个角的 MST 是常数食物间 MST 是 O(m²)。作业这个规模直接用曼哈顿版本即可在更大的自定义地图上才需要换 BFS 距离并预计算。还有一个小技巧如果节点数刚好卡线可以把优先队列的 priority 从单值改成元组例如(newCost h, -h)同分时优先展开 h 大的节点通常能再压掉一批。另外我一般会先跑 tinyCorners、trickySearch 这类小图验证正确性再上 medium 和 big 图别把搜索过程当黑匣子直接跑大图翻车了分不清是算法问题还是启发式问题。4. 避坑指南Search 项目最常见的五个翻车现场这一章全是血泪经验。我拆这套项目时把最常见的五类问题按「现象 → 原因 → 解决」列出来你照着对号入座。4.1 返回的不是动作序列autograder 直接报错现象autograder 输出 FAIL提示 path does not end at a goal state或者图形界面里吃豆人沿错误方向移动、原地打转。 原因search.py 的四个函数要求返回动作列表如[North, East]有人却返回了状态坐标列表或者把 (state, actions, cost) 整个三元组返回。autograder 从返回结果里取动作序列类型和内容都对不上。 解决检查 return 的对象必须是actions且每一项是 getSuccessors 返回的那个字符串。拿不准就在 return 前 print 一下前两个动作确认是 North/South/East/West 这类值。4.2 同一个状态反复入栈mediumMaze 卡成 PPT现象DFS 跑 mediumMaze 时展开节点数暴涨动画一帧一帧挪最后路径还特别长。 原因只做弹出时的 visited 检查push 前没去重。mediumMaze 这种有多个回路的图同一个格子会被不同路径反复压入栈里堆积大量重复节点。 解决push 前加if nxt not in visited过滤同时保留弹出时的if state not in visited兜底。前一道拦截防堆积后一道防两个父节点把同一个子节点同时压入。这套双保险在四个算法里通用。4.3 A* 的 priority 漏掉 g退化成贪心还浑然不觉现象bigMaze 上 A* 秒出路径但路径代价比 BFS 的最优值大一截换几张图路径肉眼可见地绕。 原因push 时 priority 写成了heuristic(nxt, problem)把已经走过的 g 丢了。A* 的 priority 必须是newCost heuristic(nxt, problem)缺了 g 就是贪心最佳优先搜索。 解决对照 2.3 的模板逐行检查 priority 那一行。有个自查技巧把 heuristic 换成 nullHeuristic如果展开节点数和路径代价值和 UCS 完全一致说明 g 的部分没错问题只可能出在启发式本身。4.4 启发式高估启发式「幻觉」导致路径非最优现象foodHeuristic 或 cornersHeuristic 在小图上能出解但 autograder 的 q6/q7 判 heuristic is not admissible或者路径代价高于基准值。启发式调参在早期我看来基本是玄学直到搞懂可采纳性才不亏分。 原因最常见的写法是把到所有剩余目标的距离求和。角落问题的例子在 3.2 已经说过四条曼哈顿距离相加是典型的 AI 幻觉式高估——真实路径是串行折线不是从当前位置向多个目标放射。 解决改用「最近目标距离 剩余目标间 MST」的下界结构参考 3.2 和 3.3 的代码。验证方法先跑一次 BFS 拿到最优代价再跑 A* 对比两者不一致就一定是启发式的问题再快一点的检查是手算几个状态确认 h 确实小于等于真实代价。4.5 无显示环境跑可视化窗口秒退或直接卡死现象在服务器或远程终端里执行 pacman.py要么报 no display要么图形窗口一闪而过要么整条命令卡住不动。 原因graphicsDisplay 依赖本地图形环境tkinterheadless 机器上没有 X 服务。很多人以为是代码写错了其实只是环境问题。 解决验证逻辑一律用-q静默模式只读输出的路径代价和节点统计要看搜索过程就在本机跑并加--frameTime 0加速。本机缺 tkinter 的就装上对应系统包比如 Ubuntu 下sudo apt install python3-tk。注意无显示环境下千万别去掉-q否则 autograder 也会因为创建不了窗口而挂掉。5. 把 autograder 当回归测试验收、自定义地图与复盘习惯5.1 autograder 与节点数硬指标官方评分脚本是一等一的回归测试工具python autograder.py -q q1 # 只测 DFS python autograder.py -q q7 # 只测 foodHeuristic python autograder.py # 全量跑 q1-q7每个 question 内部有多条测试用例会检查正确性、最优性和展开节点数。我的习惯是每写完一个函数就跑一次对应 -q全绿再动下一个改 searchAgents.py 之前先全量跑一遍存基线防止调启发式时把 search.py 带崩。5.2 自己造一张 layout验证不再靠猜官方 layouts 目录下的 .lay 是纯文本符号约定%是墙、空格是空地、P是吃豆人起点、.是食物、G是幽灵起点。手搓一张测角落启发式的小图%%%%% % % %.P % % . % %%%%%保存成 layouts/myTest.lay。注意两个硬要求每一行长度必须一致不足补空格四周必须用 % 围死否则 pacman.py 解析会出错或者越界。然后python pacman.py -l myTest -p SearchAgent -a fnastar,probCornersProblem,heuristiccornersHeuristic从那以后我每次写完启发式都强制走一遍流程autograder 全量基线 → 小图人工验证 → 大图压测节点数 → 再提交。这套流程帮我省掉的返工次数比任何教程都值特别是 q7 卡 700 节点的阶段没有基线根本分不清是下界松了还是算法写坏了。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

大模型算法之后,为什么产品经理成了最热门的岗位?TaoToken视角下的AI产品经理NPDP能力拆解 2026/9/25 14:05:10

大模型算法之后,为什么产品经理成了最热门的岗位?TaoToken视角下的AI产品经理NPDP能力拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
TVA具身智能运行机理(44):适配国产NPU核心技巧解析 2026/9/25 14:04:58

TVA具身智能运行机理(44):适配国产NPU核心技巧解析

前沿技术探索:TVA智能体(简称TVA)TVA智能体(亦称“AI智能体视觉”)是依托Transformer架构与“因式智能体”理论构建的新型工业视觉系统,也是当前最具代表性的具身视觉技术之一。它有机融合深度强化学习&…

阅读更多 →
黄白助手 第 059 个开关:启用随机尾巴来源的位置、验证方法与风险边界 2026/9/25 14:04:51

黄白助手 第 059 个开关:启用随机尾巴来源的位置、验证方法与风险边界

🔥 个人主页: 杨利杰YJlio ❄️ 个人专栏: 《Windows 疑难杂症与工单复盘案例库》 《Sysinternals实战教程》 《WINDOWS教程》 《Windows PowerShell 实战》 《IOS插件分析测试》 《超简单:用Python让Excel飞起来》…

阅读更多 →
自从用上Claude Code后,敲代码真的好简单:TaoToken统一Key接入与settings.json配置实战 2026/9/25 14:04:45

自从用上Claude Code后,敲代码真的好简单:TaoToken统一Key接入与settings.json配置实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Codex 重置次数查询 Skill:把过期时间写进 config.toml 骨架 2026/9/25 14:04:38

Codex 重置次数查询 Skill:把过期时间写进 config.toml 骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
项目实训5——AI Coding工具切换:用CC Switch统一管理Claude Code配置与TaoToken接入 2026/9/25 14:04:38

项目实训5——AI Coding工具切换:用CC Switch统一管理Claude Code配置与TaoToken接入

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉