☰
AI搜索算法实战:从罗马尼亚问题到Wumpus世界与蚁群算法
2026/10/20 13:26:45 网站建设 项目流程

简介:这份资源是人工智能课程配套的完整代码合集,面向正在学习AI基础算法与经典实验的高校学生及自学者,覆盖搜索、博弈、强化学习与深度学习等核心模块。包内共50个文件,以20个Python源码为主体,辅以png结果图、xml工程配置、gz与idx格式的MNIST数据集、pth模型权重、xlsx启发函数表及txt说明等,压缩包约22.55MB,按work1至work8分目录组织,结构清晰便于逐项对照学习。内容涵盖罗马尼亚问题的代价一致宽度优先、贪婪与A*搜索,以及蚁群算法求解;8皇后问题、Wumpus怪兽世界的联机搜索与强化学习、α-β剪枝井字棋、MCYTS算法,并延伸至LeNet-5手写数字识别,多数实验配有GUI可视化界面与收敛曲线。已有109人学习下载,适合作为课程作业参考、算法对比实验与期末复习的实操素材。

1. 从罗马尼亚问题到 Wumpus 世界:一套 AI 搜索算法作业到底在练什么

很多人第一次打开人工智能课程的代码包,看到罗马尼亚问题、8 皇后、Wumpus 怪兽世界这几个词并列在一起,第一反应是“这都什么年代的东西”。但真正把代价一致搜索、贪婪算法、A*、联机搜索、蚁群算法这几套代码跑一遍,你会发现它们其实在训练同一件事:把现实问题抽象成状态空间,再在状态空间里做有依据的取舍。罗马尼亚问题练的是有向图上的路径规划,8 皇后练的是约束满足,Wumpus 世界练的是部分可观测下的联机决策,蚁群算法则是把启发式从“单 agent 贪心”扩展到“群体信息素正反馈”。这套作业适合正在上 AI 课、需要交代码又要能讲清楚原理的人,也适合工作后想补搜索算法底子的工程师。下面我按“能复现”的标准,把每个模块的选型理由、参数设置和踩坑点拆开讲。

2. 罗马尼亚问题:代价一致、贪婪与 A* 的代码骨架与参数差异

2.1 为什么同一个地图要写三套搜索

罗马尼亚问题常见做法是给出一张城市连接图,边权是公里数,起点通常选 Arad,终点 Bucharest。代价一致搜索(UCS)按累计代价 g(n) 扩展,保证最优但盲目;贪婪最佳优先只按启发式 h(n) 扩展,快但可能绕远;A* 用 f(n)=g(n)+h(n),在 h 可采纳的前提下既最优又比 UCS 少扩展节点。三套代码共用同一份图数据和同一个优先队列接口,区别只在排序键。我一般会把图写成邻接表,启发式用直线距离表,这样切换算法只改一个 key 函数。

import heapq # 罗马尼亚问题邻接表:城市 -> [(邻居, 公里数)] graph = { 'Arad': [('Zerind', 75), ('Sibiu', 140), ('Timisoara', 118)], 'Zerind': [('Arad', 75), ('Oradea', 71)], 'Sibiu': [('Arad', 140), ('Fagaras', 99), ('Rimnicu', 80)], 'Timisoara': [('Arad', 118), ('Lugoj', 111)], 'Fagaras': [('Sibiu', 99), ('Bucharest', 211)], 'Rimnicu': [('Sibiu', 80), ('Pitesti', 97), ('Craiova', 146)], 'Pitesti': [('Rimnicu', 97), ('Bucharest', 101), ('Craiova', 138)], 'Bucharest': [('Fagaras', 211), ('Pitesti', 101)], } # 直线距离启发式,必须 <= 真实最短距离才可采纳 h = { 'Arad': 366, 'Zerind': 374, 'Sibiu': 253, 'Timisoara': 329, 'Fagaras': 176, 'Rimnicu': 193, 'Pitesti': 100, 'Bucharest': 0, } def search(start, goal, mode='astar'): # 优先队列元素: (优先级, 累计代价, 当前节点, 路径) frontier = [(0, 0, start, [start])] visited = {} while frontier: _, g, node, path = heapq.heappop(frontier) if node == goal: return path, g if node in visited and visited[node] <= g: continue visited[node] = g for nxt, cost in graph.get(node, []): ng = g + cost if mode == 'ucs': prio = ng elif mode == 'greedy': prio = h.get(nxt, 0) else: prio = ng + h.get(nxt, 0) heapq.heappush(frontier, (prio, ng, nxt, path + [nxt])) return None, float('inf') print(search('Arad', 'Bucharest', 'ucs')) print(search('Arad', 'Bucharest', 'greedy')) print(search('Arad', 'Bucharest', 'astar'))

这段代码里mode决定优先级计算方式,visited记录已扩展节点的最小 g 值,防止同一节点被更差路径重复入队。参数上最关键的是启发式表:如果 h 填得比真实距离大,A* 就不再保证最优,这是最常见的翻车点。另外优先队列里我放了(prio, g, node, path),Python 的 heapq 在 prio 相同时会比较第二个元素,所以 g 放在第二位能避免比较节点字符串时报错。

2.2 三套算法的扩展节点数对比与调参

跑通之后不要只看最终路径,要看扩展节点数。UCS 在罗马尼亚图上通常会扩展 10 个以上节点,贪婪可能只扩展 4 到 5 个但路径不是最优,A* 一般扩展 6 到 7 个且路径最优。如果你把 h 表整体乘 1.2,A* 会变快但可能丢最优,这叫加权 A*,工程上用来换时间。我一般会加一个计数器统计heappop次数,输出成表格对比。

算法排序键最优性典型扩展节点数适用场景
UCSg(n)保证10+边权非负、无启发式
贪婪h(n)不保证4~5实时性优先、可接受次优
A*g+hh可采纳时保证6~7有可靠启发式
加权A*g+1.2h不保证5~6时间紧、允许误差

调参时注意:h 表必须和 graph 的节点名完全一致,大小写、空格都会导致h.get(nxt, 0)返回 0,退化成 UCS。另一个坑是visited判断放在heappop之后而不是heappush之前,前者允许同一节点多次入队但只扩展一次,后者会过早剪枝导致丢最优,这是血泪经验。

3. 8 皇后与 Wumpus 世界:约束满足和联机搜索的落地写法

3.1 8 皇后用回溯还是局部搜索

8 皇后是约束满足问题,常见两种写法:回溯法逐行放皇后并检查列和对角线冲突,或者用最小冲突启发式的局部搜索。回溯法代码短、一定能找到解,适合交作业;局部搜索在 n 很大时更快,但可能卡在局部最优。我一般先写回溯,再加一个冲突计数函数,方便后面改成局部搜索。

def solve_8queens(n=8): cols = set() # 已占用列 diag1 = set() # 主对角线 r-c diag2 = set() # 副对角线 r+c board = [] def backtrack(row): if row == n: return True for c in range(n): if c in cols or (row - c) in diag1 or (row + c) in diag2: continue cols.add(c); diag1.add(row - c); diag2.add(row + c) board.append((row, c)) if backtrack(row + 1): return True board.pop() cols.remove(c); diag1.remove(row - c); diag2.remove(row + c) return False backtrack(0) return board print(solve_8queens(8))

这里用三个集合做 O(1) 冲突检查,比每次扫描棋盘快一个量级。参数 n 改成 4 可以快速验证逻辑,改成 12 以上回溯会明显变慢,这时候就该换局部搜索。注意board.pop()和三个集合的 remove 必须成对出现,漏一个就会污染后续分支,这是递归回溯里最容易犯的错。

3.2 Wumpus 世界的联机搜索怎么建模

Wumpus 世界是一个部分可观测环境:agent 在网格里移动,感知到 breeze(相邻有坑)、stench(相邻有怪兽)、glitter(当前有金子)。联机搜索算法(如联机 DFS 或联机 A*)的特点是 agent 不知道全局地图,只能边走边建图,遇到死路要回溯。常见做法是维护一个visited集合、一个safe集合和一个frontier列表,每次只往已知安全且未访问的格子走。

# 简化的 Wumpus 联机探索骨架 def online_explore(start, perceive, move): visited = {start} safe = {start} frontier = [start] path = [start] while frontier: cur = path[-1] # 感知当前格,标记相邻安全格 p = perceive(cur) for nb in neighbors(cur): if nb not in visited and is_safe(p, nb): safe.add(nb) # 找一个安全且未访问的邻居 nxt = next((n for n in neighbors(cur) if n in safe and n not in visited), None) if nxt: move(cur, nxt) visited.add(nxt) path.append(nxt) else: path.pop() # 回溯 if path: move(cur, path[-1]) return visited

perceive返回当前格的感知元组,is_safe根据 breeze 和 stench 判断邻居是否安全。联机搜索的核心是“只走已知安全格”,任何未验证的格子都不能进,否则可能掉坑。参数上frontier其实可以用栈或队列控制探索顺序,栈是 DFS 风格,队列是 BFS 风格,前者省内存后者路径短。

3.3 联机搜索的失败模式与调试手段

联机搜索最容易翻车的地方是感知函数写错,比如把 stench 判断成 breeze,导致 agent 把怪兽格当安全格走进去。调试时我一般会打印每一步的visited、safe和path,人工核对是否出现“未感知却标记安全”的格子。另一个坑是回溯时没有更新path,agent 会原地打转。建议加一个步数上限,超过就报错退出,避免死循环。

4. 罗马尼亚问题遇上蚁群算法:从单 agent 到群体正反馈

4.1 蚁群算法在路径规划上的映射

蚁群算法(ACO)原本用来解 TSP,搬到罗马尼亚问题上就是把城市当节点、边当路径,多只蚂蚁并行构造路径,信息素留在边上。每只蚂蚁按概率选择下一个城市,概率正比于(信息素^α) * (1/距离^β)。一轮结束后,按路径长度更新信息素,短的路径加得多,同时所有边按挥发率蒸发。这样短路径上的信息素会逐渐累积,蚂蚁越来越倾向走好路。

import random def aco_romania(graph, start, goal, n_ants=10, n_iter=50, alpha=1.0, beta=2.0, rho=0.5, q=100): # 初始化信息素 pheromone = { (u, v): 1.0 for u in graph for v, _ in graph[u] } best_path, best_cost = None, float('inf') for _ in range(n_iter): all_paths = [] for _ in range(n_ants): path, cost = [start], 0 cur = start while cur != goal and len(path) < len(graph): cands = [(v, c) for v, c in graph[cur] if v not in path] if not cands: break weights = [] for v, c in cands: tau = pheromone.get((cur, v), 1.0) eta = 1.0 / c weights.append((tau ** alpha) * (eta ** beta)) total = sum(weights) probs = [w / total for w in weights] nxt, c = random.choices(cands, weights=probs)[0] path.append(nxt); cost += c; cur = nxt if cur == goal: all_paths.append((path, cost)) if cost < best_cost: best_cost, best_path = cost, path # 挥发 for k in pheromone: pheromone[k] *= (1 - rho) # 增强 for path, cost in all_paths: for i in range(len(path) - 1): pheromone[(path[i], path[i+1])] += q / cost return best_path, best_cost print(aco_romania(graph, 'Arad', 'Bucharest'))

alpha控制信息素权重,beta控制距离权重,rho是挥发率,q是信息素增量系数。常见设置是 alpha=1、beta=2~5、rho=0.3~0.7。beta 太大退化成贪婪,太小则信息素主导、容易早熟。n_ants一般取节点数的 1~2 倍,n_iter看收敛曲线,通常 50 到 100 轮足够。

4.2 蚁群算法的参数敏感性与收敛判断

ACO 对参数很敏感,我一般会固定随机种子跑 5 次取平均,避免单次运气好。收敛判断看best_cost是否连续多轮不变,如果 10 轮没下降就可以停。另一个坑是信息素初始化全为 1 时,第一轮完全靠距离启发,如果 beta 设得太小,第一轮路径会很差,后续信息素被差路径污染,需要更多轮才能纠正。建议 beta 初始设 3 以上。

参数作用常用范围调大后果调小后果
alpha信息素权重0.5~2早熟收敛退化为随机
beta距离权重2~5接近贪婪收敛慢
rho挥发率0.3~0.7遗忘快、不稳定信息素堆积
q增量系数50~200信息素差异大差异不明显

4.3 和 A* 的对比:什么时候用蚁群

A* 在罗马尼亚这种小图上几乎瞬间出最优解,蚁群反而要跑几十轮。蚁群的价值在于图很大、边权动态变化、或者需要多目标权衡时,它能通过信息素并行探索。如果只是交作业,A* 足够;如果想展示群体智能,蚁群是加分项。我一般会两个都写,用同一份图数据,输出路径和代价做对比。

5. 避坑与排查:这套 AI 搜索作业最常见的 5 个翻车点

5.1 启发式不可采纳导致 A* 丢最优

现象:A* 跑出来的路径比 UCS 长。原因:h 表里某个城市的值大于真实最短距离,f(n) 被高估,优先队列提前把差路径弹出。解决:用 UCS 跑一遍得到每个城市的真实代价,确保 h 不超过它;或者把 h 整体缩小 10% 做保守估计。

5.2 优先队列元素不可比较导致 TypeError

现象:heapq 在 prio 相同时报TypeError: '<' not supported between instances of 'str' and 'str'。原因:元组第二项是节点名字符串,Python 会继续比较。解决:把 g 值放在第二项,节点名放第三项,保证前两项可比较。

5.3 8 皇后回溯忘记恢复状态

现象:n=8 时无解或解的数量不对。原因:cols、diag1、diag2在回溯分支里 add 了但没 remove。解决:把 add 和 remove 写成成对操作,或者用try/finally保证恢复。

5.4 Wumpus 联机搜索把未验证格当安全格

现象:agent 走进怪兽格或坑里。原因:is_safe逻辑写反,或者把“没有感知到危险”当成“安全”。解决:只有明确感知到相邻无 breeze 且无 stench 的格子才能标记安全,其余一律视为未知。

5.5 蚁群算法信息素更新遗漏边

现象:某些边信息素一直是初始值,蚂蚁从不走。原因:更新时只遍历了all_paths里出现的边,没出现的边只挥发不增强。解决:挥发阶段遍历所有边,增强阶段只增强走过的边,这样没走过的边信息素会逐渐降低,符合预期。

6. 把联机搜索和蚁群接起来:一个可验证的进阶技巧

如果你已经把前面几块跑通,可以试一个进阶玩法:用蚁群算法给 Wumpus 世界的联机搜索生成“先验信息素图”。具体做法是先在已知地图上跑 ACO,得到每条边的信息素值,然后把信息素作为联机搜索的启发式权重——信息素高的方向优先探索。这样联机 agent 在未知环境里不是盲目 DFS,而是带着“历史经验”走。验证方法是固定随机种子,对比纯联机 DFS 和带信息素先验的联机搜索,看平均步数和掉坑次数。

# 用 ACO 信息素初始化联机搜索的探索顺序 def aco_guided_explore(start, perceive, move, pheromone): visited, safe, path = {start}, {start}, [start] while True: cur = path[-1] p = perceive(cur) for nb in neighbors(cur): if nb not in visited and is_safe(p, nb): safe.add(nb) # 按信息素从高到低选下一个安全未访问格 cands = [n for n in neighbors(cur) if n in safe and n not in visited] if cands: nxt = max(cands, key=lambda n: pheromone.get((cur, n), 0)) move(cur, nxt); visited.add(nxt); path.append(nxt) else: path.pop() if not path: break move(cur, path[-1]) return visited

这个技巧的关键是信息素图必须来自同一张地图的离线 ACO,否则先验就是噪声。我一般会跑 3 组对比:纯 DFS、纯 A* 离线、ACO 引导联机,记录步数和成功率。实测下来 ACO 引导在中等规模地图上能减少 20% 到 30% 的无效探索,但地图太小时优势不明显,因为 DFS 本来就很快。

最后说个我自己的习惯:每写完一个搜索算法,先拿 3 个节点的最小图跑一遍,确认路径和代价都对,再换 8 节点、15 节点逐步放大。这样出问题时能快速定位是算法逻辑错还是数据规模导致的性能问题。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询