机器人寻路算法详解:方向字典 + BFS 最短路径 + 边界探索
Python · 算法 · 广度优先搜索
摘要:本文围绕一段机器人自动寻路的代码,按“定义规则 → 核心算法 → 兜底策略”三步拆解:用方向字典把现实方位翻译成坐标加减;用广度优先搜索(BFS)求出带朝向约束的最短动作序列;再用“前沿探索”解决终点未知、道路被堵的情况(局限:禁止右转)
---------------------------------------------------------------------------------------------------------------------------------
文章目录
1. 前言:机器人是怎么“看见”世界的
2. 第一步:定义规则 —— 机器人的“世界观”
3. 第二步:核心算法 —— 机器人的“探索大脑”(BFS)
4. 第三步:兜底策略 —— 机器人的“聪明策略”
---------------------------------------------------------------------------------------------------------------------------------
1. 前言:机器人是怎么“看见”世界的
机器人站在网格地图的某个格子上,用 (x, y) 记坐标,同一时刻只朝东、南、西、北中的一个方向,每一步只能前进、左转、右转或后转。地图上有墙壁、有边界,还有一大片从未去过的未知区域;关键设定是它看不到全局地图,只知道自己这一格四周能不能走。 于是任务被拆成两层:已知终点时怎么走最短,以及终点未知或路径被堵死时怎么办。前者交给 BFS,后者交给前沿探索。整段代码因此只有三块 —— 一张方向字典、一个 _plan 搜索函数、一个 isFrontier 兜底判断。
2. 第一步:定义规则 —— 机器人的“世界观”
原理:把“方位”翻译成“坐标增量”。
人用东南西北思考,计算机只会做加减,DELTA 就是这张翻译表。

|
方向 |
坐标增量 (dx, dy) |
含义 |
|
东 |
(+1, 0) |
列号 +1,向右一格 |
|
南 |
(0, +1) |
行号 +1,向下一格 |
|
西 |
(-1, 0) |
列号 -1,向左一格 |
|
北 |
(0, -1) |
行号 -1,向上一格 |
三个字典各管一件事:
DELTA 管“走一步”的位移,只与方向有关
TURN_* 管“转一下”之后的新朝向,只与当前朝向有关
BLOCKED 把“墙壁”和“边界”归成同一类不可通行物,判断时不必分情况写 if
方向写成查表之后,寻路函数里只剩“位置 + 朝向”两种数据,动作被压缩成一次字典取值;将来扩展成八方向,也只改这张表,算法一行都不用动
3. 第二步:核心算法 —— 机器人的“探索大脑”(BFS)
原理:把“位置 + 朝向”当成一个不可分割的状态,从起点逐层向外扩散。每个动作的代价都是 1 步,队列的先进先出特性保证:第一次被弹出并判定为终点的状态,一定是步数最少的。这就是广度优先搜索

|
代码位置 |
写法 |
作用 |
为什么必须这样 | ||
|
状态 |
(pos, d) |
把位置和朝向捆成一个搜索节点 |
同一格朝向不同,能走的下一步就不同,只记位置会被误判成同一个点 | ||
|
parent |
{start: None} |
记录“从哪来、做了什么动作”,同时兼作 visited |
key 存在即访问过,省掉一个 set | ||
|
出队 |
queue.popleft() |
队首出队,逐层扩散 |
换成 pop() 就变成深度优先,找到的路不再最短 | ||
|
终判 |
pos != startPos and isGoal(pos) |
命中终点就回溯 |
不排除起点,会立刻返回空路径,被误判成“找到了” | ||
|
扩展 |
candidates 三元组 |
前进 / 左转 / 后转统一成 (位置, 朝向, 动作) |
循环体里不再分情况,扩展只有五行 | ||
|
剪枝 |
if state in parent: continue |
同一状态只入队一次 |
否则队列会指数膨胀 | ||
|
判墙 |
nextPos not in openSet |
挡住墙壁和边界 |
放在入队之前,队列里不会出现垃圾状态 | ||
|
回溯 |
actions.reverse() |
把倒着收的动作翻正 |
顺着 parent 往回走是从终点到起点 | ||
|
指标 |
结论 |
说明 | |||
|
状态总数 |
≤ 4 × 可走格数 N |
每个格子最多 4 种朝向 | |||
|
每状态扩展 |
≤ 3 个动作 |
前进 / 左转 / 后转;右转连做三次左转等价,省掉这一支 | |||
|
时间复杂度 |
O(N) |
每个状态只出队一次 | |||
|
空间复杂度 |
O(N) |
parent 与队列各占一份 | |||
· 状态 = (位置, 朝向):只记位置不够。站在同一格,面朝东和面朝北可走的下一步完全不同,必须带上朝向,否则两条不同的路会被误判成同一个点
· popleft() 是 BFS 的灵魂:换成 pop() 就退化成深度优先,找到的路不再是最短路径
· pos != startPos:排除“起点本身就是终点”,否则会立刻返回一条空路径,被上层误判成“已经找到了”。
4. 第三步:兜底策略 —— 机器人的“聪明策略”
原理:BFS 本身不关心“终点”是什么,它只认一个判定函数 isGoal。既然函数能当参数传,就可以把“去终点”和“去任何没看过的地方”都写成判定函数,复用同一个 _plan。

|
优先级 |
传入的判定函数 |
触发条件 |
机器人做什么 |
|
① 直奔终点 |
lambda p: p == (tx, ty) |
知道终点坐标,且路径可达 |
取最短路径的第一步 |
|
② 去开地图 |
isFrontier |
① 返回 None(路被堵死) |
走向“四周还有未知格”的格子,像战争迷雾一样推图 |
|
③ 原地转身 |
无,直接 return "左转" |
② 也返回 None(未知区域探完了) |
换个朝向,下一帧再试 —— 宁可原地转,绝不卡死 |
三档策略的返回结构是统一的:都是一个动作字符串。上层只管执行,不必知道它来自搜索、探图还是兜底 —— 接口统一,上层逻辑就永远只有“拿到动作 → 执行”两行。
5. 调用方:吃金币与记忆初始化
原理:_plan 只负责“从A点到B点怎么走”,但机器人每一步真正要回答的是“现在该做什么动作”。答案按优先级分两层:眼前有金币就先吃(不需要搜索),没有就找一个记过的金币走过去(调用 _plan)。而 memory 里的 coinCells 就是“记过的金币”这张清单。
代码(吃金币的决策函数)

代码(记忆初始化与金币登记)

上段代码的四处关键写法
|
代码位置 |
写法 |
为什么这样写 |
|
记忆自检 |
not memory or not isinstance(memory[0], dict) |
跨回合运行时 memory 可能是旧格式或空值,不清空会直接抛异常 |
|
金币清单 |
memory[0].setdefault("coin", []) |
memory[0] 原本只有 open/wall,setdefault 一次完成“取 + 补” |
|
金币注销 |
if (x, y) in coinCells: coinCells.remove((x, y)) |
走到金币格即视为吃掉,从清单划掉,避免回头再绕一次 |
|
代价 |
整段是 O(1) 的列表操作 |
这一步放在每帧开头,不能有搜索开销 |
吃金币决策函数的四处关键写法
|
代码位置 |
写法 |
为什么这样写 |
|
感知顺序 |
("front", …), ("left", …), ("back", …), ("right", …) |
front 最省事排第一;命中即 return,不会白算后面的 |
|
右转禁发 |
("right", "左转") |
学号尾号 9 禁用右转,看到金币也只发左转,宁多转一次不违规 |
|
候选排序 |
sorted(coinCells, key=lambda p: abs(p[0]-x)+abs(p[1]-y)) |
按曼哈顿距离由近到远,先试最可能走通的 |
|
闭包绑定 |
lambda p, c=c: p == c |
写成 lambda p: p == c 会让所有判定都指向最后一枚金币 |
|
失败继续 |
if actions: return actions[0] |
金币被墙围住就换下一个,不能让机器人死等 |
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)