Part IV · 第 14 章 运动规划 · 14.1Part IV · Ch. 14 Motion Planning · 14.1

图搜索与 A*Graph Search & A*

把地图切成格子、把构型空间离散成状态,运动规划就变成了在图上找一条最便宜的路。Dijkstra 按“离起点多远”一圈圈向外扩展;A* 再加一个“离终点大概还有多远”的估计,把搜索拉向目标,只要估计从不高估,结果仍是最优的。本节一步步运行 BFS、Dijkstra 与 A*,看启发式的好坏、加权 A* 的速度与代价、栅格连通性对路径质量的影响,再看汽车用的 Hybrid A* 与状态格,以及边走边发现障碍时如何用 D* Lite 增量重规划。 Cut the map into cells, discretize the configuration space into states, and motion planning becomes finding the cheapest route through a graph. Dijkstra expands ring by ring in order of distance from the start; A* adds an estimate of "roughly how far to the goal" that pulls the search toward the target, and as long as the estimate never overestimates, the answer is still optimal. This lesson runs BFS, Dijkstra and A* step by step, then looks at what makes a heuristic good, the speed and price of weighted A*, how grid connectivity shapes path quality, Hybrid A* and state lattices for cars, and how D* Lite replans incrementally when obstacles are discovered on the way.

约 40 分钟~40 min 前置:6.12 动态规划 · 7.2 构型空间 · 9.1 移动机器人与非完整约束Prereqs: 6.12 Dynamic programming · 7.2 Configuration space · 9.1 Mobile robots & nonholonomic constraints 6 个交互6 interactives

    1为什么机器人需要它Why robots need this

    仓库里的搬运机器人要从货架 A 开到打包台,扫地机器人要绕过沙发回到充电座,自动驾驶汽车要从车道开进一个车位,机械臂要从一个姿势摆到另一个姿势而不碰到桌子。它们共同的问题是运动规划:在构型空间的自由部分 \(\mathcal C_{\text{free}}\) 里,找一条从起点到终点的路径,最好还是最短、最省时或最安全的那条。

    最直接的办法是把连续的空间离散化:把地图切成格子(每个空闲格是一个节点,相邻格之间连一条边,边上写着走过去的代价),问题就变成了在一张图上找最短路。6.12 节已经用值迭代在格子世界里算出过“每个格子到终点的最优代价” \(V\),那是对所有起点一次性算完。可是机器人通常只关心一个起点:此刻它在哪。这时没必要把整张表算出来,只要从起点出发,按某种聪明的顺序往外探,探到终点就停。这种顺序的选择,就是 BFS、Dijkstra 与 A* 的区别。

    A* 是 1968 年为 SRI 的移动机器人 Shakey 发明的,五十多年后仍然是 ROS 导航栈、游戏寻路和自动驾驶泊车规划的核心。本节不仅讲它为什么对、为什么快,也讲在真实机器人上用它时要面对的几件事:启发式怎么选、能不能为了速度牺牲一点最优性、栅格路径为什么歪歪扭扭、汽车不能原地转向怎么办、地图边走边变时怎么不必从头再来。

    A warehouse robot must drive from shelf A to the packing station, a vacuum robot must get round the sofa back to its dock, a self-driving car must get from the lane into a parking space, an arm must swing from one posture to another without hitting the table. Their shared problem is motion planning: in the free part \(\mathcal C_{\text{free}}\) of the configuration space, find a path from start to goal, preferably the shortest, fastest or safest one.

    The most direct approach is to discretize the continuous space: cut the map into cells (every free cell a node, an edge between neighbouring cells labelled with the cost of moving across), and the problem becomes a shortest path in a graph. Lesson 6.12 already computed, by value iteration on a grid world, "the optimal cost from every cell to the goal" \(V\), for all starts at once. But a robot usually cares about one start: where it is right now. Then there is no need for the whole table: start from the robot, probe outward in some clever order, and stop when the goal is reached. The choice of that order is what separates BFS, Dijkstra and A*.

    A* was invented in 1968 for SRI's mobile robot Shakey, and more than fifty years later it is still at the heart of the ROS navigation stack, game path-finding and parking planners for self-driving cars. This lesson covers why it is correct and why it is fast, and also what you face when using it on a real robot: how to choose the heuristic, whether to trade a little optimality for speed, why grid paths zig-zag, what to do about a car that cannot turn on the spot, and how to avoid starting over when the map changes as you drive.

    2直觉:永远先处理“最有希望”的那个节点Intuition: always process the most promising node next

    看下面这张小路网:10 个路口,路的长度就是两点间的直线距离(虚线那条是土路,同样长度要付 1.5 倍代价)。从 S 到 G 怎么走最短?

    Dijkstra 算法维护两个集合。开放集(open list,也叫“前沿”)放的是“已经找到一条路能到、但还不确定是不是最短”的节点,每个节点记着目前已知的从起点出发的最小代价 \(\hl{g}{g(n)}\);关闭集(closed list)放的是“最短代价已经确定”的节点。每一步:从开放集里取出 \(g\) 最小的节点 \(u\),把它移进关闭集;然后看它的每个邻居 \(v\),如果“经过 \(u\) 到 \(v\)”比 \(v\) 现有的记录更便宜,就更新 \(g(v)\) 并记下 \(v\) 的父节点是 \(u\)(这一步叫松弛)。终点被取出时,沿父节点倒推回去就是最短路。

    为什么取出的那一刻 \(g(u)\) 就是最短的?因为边的代价都不为负,开放集里别的节点的 \(g\) 都不比它小,任何经过它们再绕到 \(u\) 的路只会更长。所以 Dijkstra 是按“离起点的距离”从近到远、一圈圈地把节点定下来:这正是 6.12 节值迭代的“水波”,只是从起点出发、并且每个节点只处理一次。

    A* 只改了一处:取节点时不看 \(g(n)\),而看 \(f(n)=g(n)+\hl{h}{h(n)}\),其中 \(h(n)\) 是对“从 \(n\) 到终点还要多少”的估计,这里取到 G 的直线距离。远离目标的节点 \(h\) 大,就被往后排。下面一步步跑一遍,对比两者取出了多少个节点。

    Look at the small road network below: 10 junctions, each road as long as the straight line between its ends (the dashed one is a dirt road, where the same length costs 1.5 times as much). What is the shortest way from S to G?

    Dijkstra's algorithm keeps two sets. The open list (also called the frontier) holds nodes that "we have found some way to reach, but are not yet sure it is the shortest"; each remembers the smallest known cost from the start, \(\hl{g}{g(n)}\). The closed list holds nodes whose shortest cost is settled. Every step: take the node \(u\) with the smallest \(g\) out of the open list and move it to the closed list; then for each neighbour \(v\), if "reaching \(v\) through \(u\)" is cheaper than \(v\)'s current record, update \(g(v)\) and note that \(v\)'s parent is \(u\) (this is called relaxing the edge). When the goal is taken out, following the parents back gives the shortest path.

    Why is \(g(u)\) already the shortest at the moment \(u\) is taken out? Because no edge cost is negative: every other open node has a \(g\) at least as large, and any route that goes through one of them and then on to \(u\) can only be longer. So Dijkstra settles nodes ring by ring, in order of distance from the start: this is exactly the "ripple" of value iteration in Lesson 6.12, but starting from the start and processing each node only once.

    A* changes one thing: it picks nodes not by \(g(n)\) but by \(f(n)=g(n)+\hl{h}{h(n)}\), where \(h(n)\) estimates "how much more from \(n\) to the goal", here the straight-line distance to G. Nodes far from the goal have a large \(h\) and are pushed back in the queue. Step through both below and compare how many nodes each takes out.

    FIG 1路网上的 Dijkstra 与 A*:一步一步Dijkstra and A* on a road network, one step at a time
    每条路旁的数字是它的代价。青色填充的节点在关闭集里(最短代价已确定),紫色圆环的节点在开放集里,琥珀色圆环是刚被取出的节点,琥珀色细线是这一步被松弛的边。节点上方的蓝色数字是当前的 \(g\),蓝色粗线是父节点指针组成的“搜索树”。A* 模式下,紫色虚线从每个开放节点指向 G,长度就是 \(h\)。终点被取出后,琥珀色粗线是最短路。右侧表格是开放集,按排序键从小到大排列,第一行(琥珀色)就是下一个要取出的节点;键相同时先取 \(g\) 大的。可以拖动任何一个路口:路的长度跟着变。 The number beside each road is its cost. Nodes filled cyan are closed (shortest cost settled), nodes with a purple ring are open, the amber ring marks the node just taken out, and thin amber lines are the edges relaxed in this step. The blue number above a node is its current \(g\); thick blue lines are the parent pointers, the "search tree". In A* mode, purple dashed lines run from every open node to G; their length is \(h\). Once the goal is taken out, the thick amber line is the shortest path. The table on the right is the open list sorted by key, smallest first; its first row (amber) is the node that will be taken out next, with ties going to the larger \(g\). You can drag any junction: road lengths follow.
    试一试Try this
    • 先用 Dijkstra、再用 A*,各自按“单步”或“运行”一直走到 G 被取出(不要拖动路口):Dijkstra 取出了全部 10 个节点,A* 只取出 4 个,最短路一样。With Dijkstra and then with A*, press "Step" or "Run" until G is taken out (without dragging any junction): Dijkstra takes out all 10 nodes, A* only 4, and the shortest path is the same.
    • 拖动路口,让最短路不再是 S→C→E→G(比如把 E 往下拖,或把 F 拖到离 C 只有一格多的地方)。Drag a junction so that the shortest path is no longer S→C→E→G (for example, drag E downward, or drag F to about one unit from C).
    核心洞见Key insight

    三种算法是同一个循环:“从开放集里取一个节点,展开它的邻居”。唯一的区别是取哪一个:BFS 取最早进来的(先进先出),Dijkstra 取 \(g\) 最小的,A* 取 \(g+h\) 最小的。A* 的 \(f(n)\) 是“经过 \(n\) 的整条路大概要多少”的估计,所以它优先展开“看起来在最短路上”的节点。只要 \(h\) 从不高估真实剩余代价,它取出终点时找到的就仍然是最短路。

    All three algorithms are the same loop: "take a node out of the open list, expand its neighbours". The only difference is which one to take: BFS takes the earliest arrival (first in, first out), Dijkstra the smallest \(g\), A* the smallest \(g+h\). A*'s \(f(n)\) estimates "the cost of the whole route through \(n\)", so it expands nodes that "look as if they lie on the shortest path" first. As long as \(h\) never overestimates the true remaining cost, the path it has when it takes out the goal is still the shortest.

    3动手:在栅格地图上让五种搜索赛跑Play: race five searches on a grid map

    现在换成机器人最常用的图:占据栅格。每个空闲格是一个节点;“4 连通”只能上下左右走(每步代价 1),“8 连通”还能走斜线(代价 \(\sqrt2\)),但斜走时不能切过墙格的角(斜向两侧的两个直邻格都必须空闲)。走进一个格子要付该格的价格:普通格 1,绿色泥地 4(和 6.12 节一样)。墙不能走。

    栅格上 \(h\) 有几种常见选择,都按“最便宜的格子价格 1”来估计剩余距离,设 \(\Delta x,\Delta y\) 是到终点相差的格数:曼哈顿距离 \(\Delta x+\Delta y\)(4 连通空地图上恰好等于真实代价);八方向距离(octile)\(\max(\Delta x,\Delta y)+(\sqrt2-1)\min(\Delta x,\Delta y)\)(8 连通空地图上恰好等于真实代价);欧氏距离 \(\sqrt{\Delta x^2+\Delta y^2}\)(两种连通都不会高估,但比前两者更“松”)。另有两个变体:加权 A* 用 \(g+w\,h\),\(w>1\) 时更“急于”冲向终点;贪心最佳优先只看 \(h\),完全不管已经走了多远。

    Now switch to the graph robots use most: the occupancy grid. Every free cell is a node; "4-connected" moves only up, down, left and right (cost 1 per step), "8-connected" also moves diagonally (cost \(\sqrt2\)), but a diagonal step may not cut the corner of a wall cell (both cells beside the diagonal must be free). Entering a cell costs that cell's price: 1 for an ordinary cell, 4 for green mud (as in Lesson 6.12). Walls cannot be entered.

    A grid offers several common choices of \(h\), all estimating the remaining distance at the cheapest price of 1 per cell, with \(\Delta x,\Delta y\) the number of cells to the goal in each direction: the Manhattan distance \(\Delta x+\Delta y\) (exactly the true cost on an empty 4-connected map); the octile distance \(\max(\Delta x,\Delta y)+(\sqrt2-1)\min(\Delta x,\Delta y)\) (exactly the true cost on an empty 8-connected map); the Euclidean distance \(\sqrt{\Delta x^2+\Delta y^2}\) (never an overestimate for either connectivity, but looser than the other two). Two variants: weighted A* uses \(g+w\,h\), which for \(w>1\) is more "eager" to rush for the goal; greedy best-first looks only at \(h\) and ignores how far it has already come.

    FIG 2栅格搜索实验台Grid search workbench
    青色格子是已展开(关闭)的节点,紫色方框是开放集(前沿),琥珀色描边是当前正在展开的格子,琥珀色粗线是找到的路径。实心填充的格子是墙,绿色格子是泥地(价格 4)。S(蓝)和 G(红)可以拖动;在地图上点击或拖动,用图标题栏里的工具涂墙、泥地或擦除。“运行”按设定速度逐个展开节点,“完成”直接显示结果。读数里的“最优代价 C*”由 Dijkstra 在同一张图上算出,用来判断当前路径是不是最短。默认地图上,贪心最佳优先只展开 26 个格子,但它直穿泥地,路径代价 35.24,比最优的 23.73 贵 49%。 Cyan cells are expanded (closed) nodes, purple squares the open list (frontier), the amber outline is the cell being expanded now, and the thick amber line is the path found. Solid filled cells are walls, green cells mud (price 4). S (blue) and G (red) can be dragged; click or drag on the map to paint walls or mud, or to erase, with the tool in the figure's title bar. "Run" expands nodes one by one at the chosen speed; "Finish" jumps to the result. The "optimal cost C*" in the readout comes from Dijkstra on the same map and tells you whether the current path is the shortest. On the default map greedy best-first expands only 26 cells, but it ploughs straight through the mud: its path costs 35.24, 49% more than the optimal 23.73.
    • 保持 A*(八方向距离、8 连通),按“运行”:前沿朝着 G 长过去;读数里它展开的格子不到 Dijkstra 的一半(118 对 307),路径代价相同。Keep A* (octile, 8-connected) and press "Run": the frontier grows toward G; the readout shows it expands fewer than half of Dijkstra's cells (118 versus 307) at the same path cost.
    • 选 BFS:它找到的是步数最少的路,可它直穿泥地,代价比 C* 高(34.07 对 23.73)。BFS 只在每步代价都相同时才是最短路。Choose BFS: it finds the path with the fewest steps, but it walks straight through the mud and costs more than C* (34.07 versus 23.73). BFS gives shortest paths only when every step costs the same.
    • 选加权 A*,取 \(w\ge2\):展开的格子不到普通 A* 的一半,路径可能变贵,但读数里代价永远不超过 \(w\cdot C^*\)。Choose weighted A* with \(w\ge2\): it expands fewer than half of plain A*'s cells; the path may get more expensive, but the readout shows the cost never exceeds \(w\cdot C^*\).
    看到了什么What you just saw

    Dijkstra 的前沿是一圈圈“等代价线”(碰到泥地会凹进去),它不知道终点在哪个方向。A* 的前沿被拉成朝向终点的细长形状:离终点越远的格子 \(f\) 越大,排到后面一直没轮到。BFS 的前沿是“等步数线”,泥地对它不存在。贪心最佳优先几乎一条线冲过去,碰到障碍才拐弯,所以快但路差。加权 A* 介于 A* 和贪心之间:\(w\) 越大越像贪心。

    Dijkstra's frontier is a sequence of "equal-cost" rings (dented where mud slows it), with no idea which way the goal lies. A*'s frontier is stretched into a narrow shape pointing at the goal: cells far from the goal have a large \(f\) and never come up. BFS's frontier is an "equal-step-count" ring; mud does not exist for it. Greedy best-first shoots almost straight ahead and turns only at obstacles: fast, but a poor path. Weighted A* sits between A* and greedy: the larger \(w\), the more greedy.

    4另外两个视角:启发式有多好,栅格路径有多直Two more views: how good is the heuristic, how straight is a grid path

    启发式与真实剩余代价The heuristic versus the true cost-to-go

    每个格子都有一个真实的“到终点的最小代价” \(\hl{hstar}{h^*(n)}\):它就是 6.12 节的价值函数 \(V(n)\),可以从终点反向跑一遍 Dijkstra 算出来。启发式 \(h\) 是它的廉价替身,好坏可以用比值 \(h/h^*\) 衡量:处处等于 1 时 A* 只展开最短路上的格子;处处等于 0(即 \(h\equiv0\))时 A* 退化成 Dijkstra。只要 \(h\le h^*\)(称为可容许,admissible),A* 就保证最优;一旦某处 \(h>h^*\),就可能错过最短路。

    加权 A* 故意把 \(h\) 乘上 \(\hl{w}{w}>1\),让它在很多格子上高估。好消息是代价有上界:只要原来的 \(h\) 满足一致性(第 5 节),找到的路径代价不超过 \(w\) 倍最优。下图左边按 \(w\,h/h^*\) 给每个格子着色,右边扫一遍 \(w\) 从 1 到 5,画出展开数和代价比。

    Every cell has a true "minimum cost to the goal" \(\hl{hstar}{h^*(n)}\): it is the value function \(V(n)\) of Lesson 6.12, computable by running Dijkstra backward from the goal. The heuristic \(h\) is its cheap stand-in, and the ratio \(h/h^*\) measures how good it is: equal to 1 everywhere, A* expands only the cells on the shortest path; equal to 0 everywhere (\(h\equiv0\)), A* degenerates into Dijkstra. As long as \(h\le h^*\) (called admissible), A* is guaranteed optimal; as soon as \(h>h^*\) somewhere, it may miss the shortest path.

    Weighted A* deliberately multiplies \(h\) by \(\hl{w}{w}>1\), so it overestimates in many cells. The good news is that the cost has a bound: as long as the original \(h\) is consistent (Section 5), the path found costs at most \(w\) times the optimum. On the left below, each cell is coloured by \(w\,h/h^*\); on the right, \(w\) is swept from 1 to 5 and the number of expansions and the cost ratio are plotted.

    FIG 3启发式的质量与加权 A* 的代价上界Heuristic quality and the cost bound of weighted A*
    左:8 连通地图,S、G 可拖动。每个格子按 \(w\,h/h^*\) 着色:越青表示越接近真实剩余代价(越“有信息”),颜色淡表示严重低估;红色格子 \(w\,h>h^*\),即高估。蓝色小点标出这一次 A* 展开过的格子,琥珀色线是它的路径。把鼠标放在第 4、5 节公式里的 \(h^*\) 上,左图改为显示 \(h^*\) 本身(越蓝越小)。右:当前启发式下,\(w\) 从 1 扫到 5。上半是展开的格子数(蓝),下半是路径代价与最优代价之比(琥珀),紫色虚线是上界 \(w\),竖线是当前的 \(w\)。 Left: an 8-connected map; S and G can be dragged. Each cell is coloured by \(w\,h/h^*\): more cyan means closer to the true remaining cost (more "informed"), a pale tint means a severe underestimate; red cells have \(w\,h>h^*\), an overestimate. Small blue dots mark the cells this run of A* expanded, and the amber line is its path. Hover \(h^*\) in the formulas of Sections 4 and 5 and the left panel shows \(h^*\) itself instead (more blue = smaller). Right: \(w\) swept from 1 to 5 for the current heuristic. Top: the number of expanded cells (blue); bottom: path cost divided by the optimal cost (amber), with the bound \(w\) as the purple dashed line and the current \(w\) as a vertical line.
    • 在 U 形陷阱地图上用八方向距离,把 \(w\) 往上加,直到路径不再最优(代价比 > 1):此时的代价比远小于 \(w\),上界很松。On the U-trap map with the octile heuristic, raise \(w\) until the path is no longer optimal (cost ratio > 1): the ratio at that point is far below \(w\); the bound is loose.
    • 选“房间”地图和曼哈顿距离(\(w=1\)):很多格子变红,因为 8 连通时斜走一步只要 \(\sqrt2\),曼哈顿却算 2。把 S 往右拖两三格,找一个 A* 返回非最优路径的起点(代价比 > 1)。Choose the "Rooms" map and the Manhattan heuristic (\(w=1\)): many cells turn red, because on an 8-connected grid a diagonal step costs \(\sqrt2\) while Manhattan counts 2. Drag S two or three cells to the right to find a start from which A* returns a non-optimal path (cost ratio > 1).

    几点观察。(1) 在 U 形陷阱里,\(h=0\)、欧氏、八方向距离分别展开 295、132、114 个格子:\(h\) 越接近 \(h^*\),展开越少。但 U 形开口里所有格子的 \(h\) 都远小于 \(h^*\)(直线距离穿墙而过),A* 也只好把陷阱“填满”才绕出来,这是所有只看几何距离的启发式的通病。(2) 加权 A* 的代价比往往远低于上界 \(w\):上界是最坏情况的保证,不是典型值。(3) 曼哈顿距离在 8 连通栅格上不可容许,但大多数起点下它仍然碰巧给出最短路;“不可容许”说的是失去保证,不是每次都错。

    Some observations. (1) In the U-trap, \(h=0\), Euclidean and octile expand 295, 132 and 114 cells: the closer \(h\) is to \(h^*\), the fewer expansions. But every cell inside the U has \(h\) far below \(h^*\) (the straight line goes through the wall), so A* too must "fill" the trap before it escapes; every heuristic based only on geometric distance suffers from this. (2) The cost ratio of weighted A* is usually far below the bound \(w\): the bound is a worst-case guarantee, not a typical value. (3) Manhattan is inadmissible on an 8-connected grid, yet from most starts it still happens to return the shortest path; "inadmissible" means losing the guarantee, not being wrong every time.

    连通性与路径质量:栅格路径为什么不直Connectivity and path quality: why grid paths are not straight

    栅格上的“最短路”只是在允许的移动方向里最短。4 连通只能走 0°、90°……,一条 45° 方向的直线要走成阶梯,长度是 \(\hl{c4}{\Delta x+\Delta y}\),比直线长 \(\sqrt2-1\approx41\%\)。8 连通加上了斜线,长度变成 \(\hl{c8}{\max+(\sqrt2-1)\min}\),最坏出现在 22.5° 方向,长 8.2%。16 连通再加上“马步” \((2,1)\) 一类的移动,最坏只长 2.7%。更麻烦的是形状:同样代价的阶梯路径有很多条,哪条被选中取决于平局怎么打破,结果往往是不自然的折线,贴着墙角拐弯。

    任意角度(any-angle)规划不再把路径限制在栅格边上。Theta*(Nash 等,2007)在展开节点 \(u\)、更新它的邻居 \(v\) 时,检查 \(v\) 和 \(u\) 的父节点之间有没有直线视线,有就让 \(v\) 直接连到那个父节点上(跳过 \(u\)),于是路径的拐点只出现在障碍的角上。它不保证是真正的欧氏最短路,但通常非常接近。

    A "shortest path" on a grid is only the shortest among the allowed move directions. 4-connected moves go only at 0°, 90°, …, so a straight line at 45° becomes a staircase of length \(\hl{c4}{\Delta x+\Delta y}\), \(\sqrt2-1\approx41\%\) longer than the line. 8-connected adds diagonals and the length becomes \(\hl{c8}{\max+(\sqrt2-1)\min}\); the worst case is the 22.5° direction, 8.2% longer. 16-connected adds "knight" moves such as \((2,1)\), and the worst case is only 2.7%. Worse is the shape: many staircase paths have exactly the same cost, which one wins depends on how ties are broken, and the result is often an unnatural polyline that turns while hugging wall corners.

    Any-angle planning no longer restricts the path to grid edges. When Theta* (Nash et al., 2007) expands a node \(u\) and updates a neighbour \(v\), it checks whether there is a clear line of sight between \(v\) and \(u\)'s parent, and if so connects \(v\) directly to that parent (skipping \(u\)), so the path only bends at obstacle corners. It is not guaranteed to be the true Euclidean shortest path, but it is usually very close.

    FIG 44 / 8 / 16 连通与任意角度路径4-, 8-, 16-connected and any-angle paths
    拖动 S 和 G。红线是 4 连通最短路,绿线是 8 连通,蓝线是 16 连通,琥珀色是 Theta* 的任意角度路径,细虚线是 S 到 G 的直线。读数给出每条路径的长度和它比参考长度长多少:空地图上参考是直线距离(这时直线就是真正的最短路),有柱子时参考是 Theta* 的长度。把鼠标放在上文的 \(\Delta x+\Delta y\) 或八方向距离公式上,对应的路径会加粗。 Drag S and G. The red line is the 4-connected shortest path, green 8-connected, blue 16-connected, amber Theta*'s any-angle path, and the thin dashed line is the straight line from S to G. The readout gives each path's length and how much longer it is than the reference: on the empty map the reference is the straight-line distance (which is then the true shortest path), with pillars it is the length of the Theta* path. Hover \(\Delta x+\Delta y\) or the octile formula in the text above and the matching path thickens.
    • 在空地图上拖动 G,找一个方向让 8 连通路径比直线长 7% 以上(接近 22.5°);同时看 4 连通和 16 连通:前者长二三十个百分点,后者不到 3%。On the empty map drag G to a direction where the 8-connected path is more than 7% longer than the straight line (near 22.5°); look at 4- and 16-connected too: the former is twenty or thirty percent longer, the latter less than 3%.
    • 选“柱子”地图:找一个 S、G 位置,让 Theta* 比 8 连通路径短 5% 以上。Choose the "Pillars" map: find S and G positions where Theta* is more than 5% shorter than the 8-connected path.
    分辨率的两难The resolution dilemma

    格子越细,路径越接近连续最短路,也越不会把一条窄通道“看成”堵死;但格子数随维数指数增长(6.12 节的维数灾难)。一张 100 m × 100 m、5 cm 分辨率的二维地图有 400 万个格子,A* 仍然应付得了;一条 6 自由度机械臂的关节空间若每维取 100 个格点,就有 \(10^{12}\) 个格子,根本存不下。所以图搜索主宰的是低维问题(平面导航、车的 \((x,y,\theta)\)),高维的机械臂规划交给 14.2 节的采样式方法。

    The finer the cells, the closer the path gets to the continuous shortest path and the less likely a narrow passage is "seen" as blocked; but the number of cells grows exponentially with dimension (the curse of dimensionality of Lesson 6.12). A 100 m × 100 m 2D map at 5 cm resolution has 4 million cells, still manageable for A*; the joint space of a 6-DoF arm with 100 grid points per axis has \(10^{12}\) cells and cannot even be stored. So graph search rules low-dimensional problems (planar navigation, a car's \((x,y,\theta)\)), and high-dimensional arm planning goes to the sampling-based methods of Lesson 14.2.

    5再看数学The math

    提示:把鼠标悬停在公式中带底色的符号上,FIG 1–FIG 6 中对应的元素会被高亮。Tip: hover the highlighted symbols in the formulas; the matching elements of FIG 1 to FIG 6 light up.

    问题与贝尔曼方程The problem and the Bellman equation

    图 \(\mathcal G=(\mathcal V,\mathcal E)\),每条边有非负代价 \(c(u,v)\ge0\)。一条路径的代价是它各边代价之和,\(C^*\) 是从起点 \(s\) 到终点 \(t\) 所有路径中的最小代价。记 \(g^*(n)\) 为从 \(s\) 到 \(n\) 的最小代价,\(\hl{hstar}{h^*(n)}\) 为从 \(n\) 到 \(t\) 的最小代价,它们各满足一个贝尔曼方程:

    A graph \(\mathcal G=(\mathcal V,\mathcal E)\) with a non-negative cost \(c(u,v)\ge0\) on every edge. The cost of a path is the sum of its edge costs, and \(C^*\) is the minimum over all paths from the start \(s\) to the goal \(t\). Let \(g^*(n)\) be the minimum cost from \(s\) to \(n\) and \(\hl{hstar}{h^*(n)}\) the minimum cost from \(n\) to \(t\); each satisfies a Bellman equation:

    $$ \begin{gathered} g^*(s)=0,\quad g^*(v)=\min_{u}\big[g^*(u)+c(u,v)\big];\\ h^*(t)=0,\quad h^*(u)=\min_{v}\big[c(u,v)+h^*(v)\big];\\ C^*=g^*(t)=h^*(s)=\min_n\big[g^*(n)+h^*(n)\big]. \end{gathered} $$

    第二个方程正是 6.12 节的 \(V\)(\(\gamma=1\) 的最短路问题)。最后一个等式说:经过最短路上任意一点 \(n\) 的 \(g^*+h^*\) 都等于 \(C^*\),不在任何最短路上的点则严格大于 \(C^*\)。A* 的全部思想,就是用一个可计算的 \(h\) 去近似这里的 \(h^*\)。

    The second equation is exactly \(V\) of Lesson 6.12 (the shortest-path problem with \(\gamma=1\)). The last identity says that \(g^*+h^*\) equals \(C^*\) at every node \(n\) on a shortest path and is strictly larger at nodes on no shortest path. The whole idea of A* is to approximate this \(h^*\) by a computable \(h\).

    算法:一个循环,三种排序The algorithm: one loop, three orderings

    把起点放进开放集 \(\hl{open}{\mathcal O}\),\(g(s)=0\),其余 \(g=\infty\)。重复:从 \(\mathcal O\) 取出排序键最小的 \(u\),放进关闭集 \(\hl{closed}{\mathcal K}\);若 \(u=t\) 则沿父指针返回路径;否则对每个邻居 \(v\notin\mathcal K\),若 \(g(u)+c(u,v)<g(v)\),令 \(g(v)\leftarrow g(u)+c(u,v)\)、父节点 \(\pi(v)\leftarrow u\),并把 \(v\) 以新的键放进 \(\mathcal O\)。排序键:

    Put the start into the open list \(\hl{open}{\mathcal O}\) with \(g(s)=0\) and \(g=\infty\) elsewhere. Repeat: take the \(u\) with the smallest key out of \(\mathcal O\) and put it into the closed list \(\hl{closed}{\mathcal K}\); if \(u=t\), return the path by following parents; otherwise for every neighbour \(v\notin\mathcal K\), if \(g(u)+c(u,v)<g(v)\), set \(g(v)\leftarrow g(u)+c(u,v)\), the parent \(\pi(v)\leftarrow u\), and put \(v\) into \(\mathcal O\) with its new key. The keys:

    $$ \hl{g}{g(n)},\qquad f(n)=\hl{g}{g(n)}+\hl{h}{h(n)},\qquad g(n)+\hl{w}{w}\,h(n),\qquad h(n). $$

    从左到右依次是 Dijkstra、A*、加权 A* 和贪心最佳优先的键;BFS 不用键,按进入开放集的先后(先进先出)。

    From left to right: the keys of Dijkstra, A*, weighted A* and greedy best-first; BFS uses no key, only the order of arrival in the open list (first in, first out).

    开放集用二叉堆实现,复杂度为 \(O\big((|\mathcal V|+|\mathcal E|)\log|\mathcal V|\big)\);栅格上每个节点至多 8 条边。BFS 用普通队列,\(O(|\mathcal V|+|\mathcal E|)\),但只在所有边代价相同时最优(FIG 2 的泥地)。实现上通常不做“降低堆中键值”,而是把新键再压一次堆,取出时跳过过期的条目(惰性删除)。

    With a binary heap for the open list the cost is \(O\big((|\mathcal V|+|\mathcal E|)\log|\mathcal V|\big)\); on a grid every node has at most 8 edges. BFS uses a plain queue, \(O(|\mathcal V|+|\mathcal E|)\), but is optimal only when all edge costs are equal (the mud of FIG 2). Implementations usually do not "decrease a key in the heap"; they push the node again with the new key and skip stale entries when they come out (lazy deletion).

    可容许与一致:A* 为什么最优Admissible and consistent: why A* is optimal

    $$ \begin{gathered} \text{(1)}\quad 0\le \hl{h}{h(n)}\le \hl{hstar}{h^*(n)}\ \ \forall n;\\ \text{(2)}\quad h(t)=0,\ \ h(u)\le c(u,v)+h(v)\ \ \forall (u,v)\in\mathcal E. \end{gathered} $$

    (1) 是可容许,(2) 是一致。一致(也叫单调)是边上的三角不等式:估计值走一步最多下降这一步的代价。沿着从 \(n\) 到 \(t\) 的最短路把它加起来,就得到 \(h(n)\le h^*(n)\):一致蕴含可容许。直线距离、八方向距离(8 连通)、曼哈顿距离(4 连通)都是一致的。

    理解一致性最干净的方式是约化代价(reduced cost)。定义

    (1) is admissibility, (2) is consistency. Consistency (also called monotonicity) is a triangle inequality on edges: the estimate drops by at most the cost of a step. Summing it along a shortest path from \(n\) to \(t\) gives \(h(n)\le h^*(n)\): consistent implies admissible. The straight-line distance, the octile distance (8-connected) and the Manhattan distance (4-connected) are all consistent.

    The cleanest way to understand consistency is through reduced costs. Define

    $$ \begin{gathered} \tilde c(u,v)=c(u,v)-h(u)+h(v)\ \ge 0 \iff \text{(2)};\\ \tilde c(P_{s\to n})=c(P_{s\to n})-h(s)+h(n). \end{gathered} $$

    任何一条从 \(s\) 出发的路径 \(P_{s\to n}\),其约化代价只是原代价减去常数 \(h(s)\) 再加上终点的 \(h(n)\),所以到 \(t\) 的路径谁短谁长完全不变(\(h(t)=0\)),而 Dijkstra 在约化代价上的键 \(\tilde g(n)=g(n)+h(n)-h(s)=f(n)-h(s)\),与 A* 的 \(f\) 只差一个常数。结论:一致启发式下的 A* 就是边代价为 \(\tilde c\ge0\) 的 Dijkstra。Dijkstra 的一切性质随之而来:节点被取出时 \(g\) 已是最优,关闭的节点不必重开,取出的 \(f\) 值单调不减,取出 \(t\) 时路径最优。只可容许、不一致的 \(h\) 也能保证最优,但需要允许重开关闭集里的节点。

    A* 展开哪些节点。由于 \(f\) 单调不减,所有 \(g^*(n)+h(n)<C^*\) 的节点在 \(t\) 之前必然被展开,\(f>C^*\) 的节点永远不会。所以 \(h\) 越大(越接近 \(h^*\)),这个集合越小:若 \(h_1\ge h_2\) 且都一致,用 \(h_1\) 展开的节点(除平局外)是用 \(h_2\) 的子集。两个极端:\(h\equiv0\) 时是 Dijkstra;\(h=h^*\) 时 \(f\) 在最短路上处处等于 \(C^*\),配合“平局取 \(g\) 大”的规则,A* 只展开最短路上的节点。这也是“学习一个启发式”有意义的原因:价值函数本身就是最好的启发式。

    加权 A* 的上界。令 \(f_w=g+w\,h\)。取出 \(t\) 的那一刻,最短路上总有某个节点 \(n\) 在开放集里且 \(g(n)=g^*(n)\),于是

    For any path \(P_{s\to n}\) from \(s\), the reduced cost is the original cost minus the constant \(h(s)\) plus \(h(n)\) of its endpoint, so the ranking of paths to \(t\) is unchanged (\(h(t)=0\)), and Dijkstra's key on reduced costs, \(\tilde g(n)=g(n)+h(n)-h(s)=f(n)-h(s)\), differs from A*'s \(f\) by a constant. Conclusion: A* with a consistent heuristic is Dijkstra on edge costs \(\tilde c\ge0\). Every property of Dijkstra follows: \(g\) is optimal when a node is taken out, closed nodes never need reopening, the \(f\) values taken out never decrease, and the path is optimal when \(t\) is taken out. An admissible but inconsistent \(h\) also guarantees optimality, but closed nodes must then be allowed to reopen.

    Which nodes A* expands. Because \(f\) never decreases, every node with \(g^*(n)+h(n)<C^*\) is certainly expanded before \(t\), and no node with \(f>C^*\) ever is. So the larger \(h\) (the closer to \(h^*\)), the smaller this set: if \(h_1\ge h_2\) are both consistent, the nodes expanded with \(h_1\) are (up to ties) a subset of those expanded with \(h_2\). The two extremes: \(h\equiv0\) is Dijkstra; with \(h=h^*\), \(f\) equals \(C^*\) all along the shortest path and, with ties broken toward the larger \(g\), A* expands only nodes on the shortest path. This is also why "learning a heuristic" makes sense: the value function itself is the best heuristic.

    The bound of weighted A*. Let \(f_w=g+w\,h\). At the moment \(t\) is taken out, some node \(n\) of a shortest path is in the open list with \(g(n)=g^*(n)\), so

    $$ \begin{aligned} g(t)=f_w(t)\le f_w(n)&=g^*(n)+\hl{w}{w}\,h(n)\\ &\le w\big(g^*(n)+h^*(n)\big)=w\,C^*\qquad (w\ge1,\ h\le h^*). \end{aligned} $$

    这一推导在允许重开节点时直接成立;Likhachev 等(2003)证明了 \(h\) 一致时即使不重开也成立(FIG 2、FIG 3 都不重开)。所以 \(w\) 是一个旋钮:用至多 \(w\) 倍的代价,换取通常少得多的展开。ARA*(随时修复 A*)从大 \(w\) 开始,先快速给出一条路,再逐步降低 \(w\) 并复用之前的搜索,时间允许就一直改进。

    The derivation holds directly when nodes may be reopened; Likhachev et al. (2003) showed it also holds without reopening when \(h\) is consistent (FIG 2 and FIG 3 do not reopen). So \(w\) is a dial: accept at most \(w\) times the cost in exchange for usually far fewer expansions. ARA* (Anytime Repairing A*) starts with a large \(w\) to return a path quickly, then lowers \(w\) step by step while reusing the previous search, improving the path for as long as time allows.

    车的状态空间:运动基元、状态格与 Hybrid A*A car's state space: motion primitives, state lattices and Hybrid A*

    9.1 节的运动学自行车模型:后轴中心 \((x,y)\)、航向 \(\theta\),

    The kinematic bicycle model of Lesson 9.1: rear-axle centre \((x,y)\), heading \(\theta\),

    $$ \begin{gathered} \dot x=v\cos\theta,\quad \dot y=v\sin\theta,\quad \dot\theta=v\,\kappa,\\ \hl{prim}{|\kappa|\le 1/R_{\min}},\quad v\in\{+,-\}. \end{gathered} $$

    在 \((x,y)\) 栅格上,8 连通的“边”要求原地转向,车做不到。办法是把图的节点换成 \((x,y,\theta)\),边换成运动基元:在曲率约束下实际能开出来的短轨迹(直行、左转弧、右转弧,前进或倒车)。

    • 状态格(state lattice,Pivtoraiko 与 Kelly):航向只取有限几个值(FIG 5 用 8 个,每 45° 一个),每个基元都精心设计成从一个格点出发、恰好落在另一个格点上(FIG 5 的转弯基元是半径 \(R=2+2\sqrt2\approx4.83\) m 的 45° 圆弧加 0.83 m 直线,正好从 \((0,0,0°)\) 到 \((4,2,45°)\))。它仍是一张普通的图,A* 的最优性在这张图上成立;代价是表达能力受基元集合限制。
    • Hybrid A*(Dolgov 等):节点里存连续的位姿,基元直接从这个连续位姿积分出去,只用 \((x,y,\theta)\) 的栅格单元来去重:同一个单元里只保留代价最小的那个连续状态。这样每条边都严格可行,但同一单元里被丢掉的状态可能正是通往终点所需要的,所以它既不完备也不最优,只是“分辨率够细时很好用”。真实实现在接近终点时还会尝试一条 Reeds–Shepp 曲线直接解析地连到目标位姿(解析扩展),FIG 5 为简洁省略了这一步,改为允许到达目标附近 0.8 m、航向差 0.25 rad(约 14°)以内。

    启发式取两个估计的较大者(Dolgov 等称两者都可容许):考虑运动学、忽略障碍(无障碍时的 Reeds–Shepp 最短长度,可以离线制表),以及考虑障碍、忽略运动学(在二维栅格上从终点跑一次 Dijkstra)。前者在开阔处管用(车需要掉头时直线距离严重低估,9.1 节已经指出),后者把搜索从死胡同里拉出来。FIG 5 只实现了后者与直线距离的较大者。注意二维栅格距离可能比连续空间的真实距离略长(FIG 4:最多 8%),严格说并不可容许;FIG 5 因此把它减去 1.5 m 的余量再用,实践中这点误差通常被接受。

    On an \((x,y)\) grid, the 8-connected "edges" demand turning on the spot, which a car cannot do. The cure is to make the graph's nodes \((x,y,\theta)\) and its edges motion primitives: short trajectories the car can actually drive under the curvature limit (straight, left arc, right arc, forward or reverse).

    • State lattice (Pivtoraiko and Kelly): the heading takes only a few values (8 in FIG 5, one every 45°), and every primitive is designed to start on a lattice point and land exactly on another (FIG 5's turning primitive is a 45° arc of radius \(R=2+2\sqrt2\approx4.83\) m plus 0.83 m of straight line, taking \((0,0,0°)\) exactly to \((4,2,45°)\)). It is still an ordinary graph, and A*'s optimality holds on it; the price is that what it can express is limited by the primitive set.
    • Hybrid A* (Dolgov et al.): nodes store a continuous pose, primitives are integrated directly from that pose, and the \((x,y,\theta)\) grid cells are used only to prune duplicates: each cell keeps only the cheapest continuous state that reached it. Every edge is then exactly drivable, but a state discarded in a cell may be just the one needed to reach the goal, so the method is neither complete nor optimal, merely "very good when the resolution is fine enough". Real implementations also try, near the goal, a Reeds–Shepp curve that connects analytically to the exact goal pose (the analytic expansion); FIG 5 omits this for brevity and instead accepts arriving within 0.8 m and 0.25 rad (about 14°) of the goal.

    The heuristic is the larger of two estimates (Dolgov et al. call both admissible): with kinematics, without obstacles (the obstacle-free Reeds–Shepp length, which can be tabulated offline), and with obstacles, without kinematics (one Dijkstra run from the goal on a 2D grid). The first helps in the open (where the car must turn around and the straight-line distance badly underestimates, as Lesson 9.1 pointed out), the second pulls the search out of dead ends. FIG 5 implements only the larger of the latter and the straight-line distance. Note that a 2D grid distance can be slightly longer than the true continuous distance (FIG 4: by up to 8%), so strictly speaking it is not admissible; FIG 5 therefore subtracts a 1.5 m margin from it, and in practice this small error is usually accepted.

    D* Lite:搜索倒过来做,变化只修补一小块D* Lite: search backward, repair only what changed

    机器人边走边用传感器发现新障碍,每发现一次,图上就有几条边的代价变成 \(\infty\)。从头跑一遍 A* 当然可以,但大部分旧结果其实仍然有效。D* Lite(Koenig 与 Likhachev,2002)的做法:从终点向机器人方向搜索,于是 \(g(s)\) 是“从 \(s\) 到终点”的代价,机器人移动时终点不变,这些值不会作废;再给每个节点加一个一步前瞻值

    The robot discovers new obstacles with its sensors as it drives, and every discovery turns the cost of a few edges to \(\infty\). Re-running A* from scratch works, but most of the old result is still valid. D* Lite (Koenig and Likhachev, 2002) searches from the goal toward the robot, so \(g(s)\) is the cost "from \(s\) to the goal"; the goal does not move when the robot does, so these values stay meaningful. It then gives every node a one-step lookahead value

    $$ \begin{gathered} \hl{rhs}{\mathrm{rhs}(s)}=\min_{s'}\big[c(s,s')+g(s')\big],\\ k(s)=\Big[\min\big(g,\mathrm{rhs}\big)+h(s_{\text{start}},s)+k_m;\ \ \min\big(g,\mathrm{rhs}\big)\Big]. \end{gathered} $$

    \(g=\mathrm{rhs}\) 的节点叫局部一致;边代价一变,只有它两端的节点变得不一致,被放进队列。队列按两段键 \(k\) 处理不一致的节点,直到机器人所在节点一致且队列中再没有键更小的节点:效果上等价于“在新地图上重新算出了需要的那部分 \(g\)”,但只碰受影响的区域。\(h(s_{\text{start}},\cdot)\) 让修补朝机器人聚焦(像 A*),机器人每走一段,\(k_m\) 加上 \(h(s_{\text{last}},s_{\text{start}})\),使队列里旧的键仍然是下界,不必重排整个队列。D* Lite 的路径与在当前已知地图上重新跑 A* 的代价完全相同(我们在 FIG 6 两张地图、半径 2 到 6 的每一次重规划上都核对过),省的只是计算。

    Nodes with \(g=\mathrm{rhs}\) are called locally consistent; when an edge cost changes, only the nodes at its ends become inconsistent and enter the queue. The queue processes inconsistent nodes by the two-part key \(k\) until the robot's node is consistent and no smaller key remains: in effect "recomputing the needed part of \(g\) on the new map", while touching only the affected region. \(h(s_{\text{start}},\cdot)\) focuses the repair toward the robot (as in A*), and each time the robot moves, \(k_m\) grows by \(h(s_{\text{last}},s_{\text{start}})\) so that the old keys in the queue remain lower bounds without re-sorting the whole queue. D* Lite's path has exactly the same cost as re-running A* on the currently known map (we checked this at every replan of FIG 6, on both maps, for radii 2 to 6); what it saves is only computation.

    6机器人中的它:车不能原地转向,地图边走边变In robotics: cars cannot turn on the spot, and maps change as you drive

    下图是一辆 4.0 m × 1.8 m 的车,最小转弯半径 4.83 m,状态是后轴中心的 \((x,y,\theta)\)。车身用沿中心线的 3 个圆盘近似做碰撞检测(这是常见做法:圆盘到障碍的距离很便宜)。Hybrid A* 每次展开生成 5 种曲率 \(\times\) 前进/倒车的 1.6 m 弧,用 1 m × 1 m × 5°(72 个航向)的单元去重;代价是路程,倒车的路程乘 2,每次换挡加 3 m,打方向的变化也有小惩罚。

    Below is a 4.0 m × 1.8 m car with a 4.83 m minimum turning radius, its state the rear-axle centre \((x,y,\theta)\). For collision checking the body is approximated by 3 discs along its centre line (a common trick: disc-to-obstacle distances are cheap). Each Hybrid A* expansion generates 1.6 m arcs at 5 curvatures \(\times\) forward/reverse and prunes duplicates in 1 m × 1 m × 5° cells (72 headings); the cost is distance travelled, with reverse distance counted double, 3 m added for every change of direction and a small penalty for changing the steering.

    FIG 5Hybrid A* 与状态格:给车找一条开得出来的路Hybrid A* and a state lattice: finding a path a car can drive
    场地 30 m × 18 m,实心矩形是障碍。蓝色车框是起点,绿色虚线车框是目标:拖动车框里后轴中心的圆点移动它,拖动车头前方的小圆点改变航向(状态格模式下会吸附到整数格点和 45° 的倍数)。青色细线是搜索树里前进的基元,紫色细线是倒车的基元。找到的路径中,琥珀色段是前进,红色段是倒车,路径上每隔一段画一个车身轮廓;随后一辆琥珀色的车沿路径开一遍。“显示栅格 A*”叠加一条把车当作点、在 1 m 栅格上 8 连通搜出来的路径(蓝色虚线):它在每个拐点要求原地转向,车开不出来。把鼠标放在第 5 节的曲率约束上,起点处会画出一次展开的全部基元。 The site is 30 m × 18 m; solid rectangles are obstacles. The blue car outline is the start, the dashed green outline the goal: drag the dot at the rear-axle centre inside an outline to move it, the small dot ahead of its nose to change its heading (in lattice mode they snap to integer lattice points and multiples of 45°). Thin cyan lines are forward primitives in the search tree, thin purple lines reverse ones. In the path found, amber segments drive forward and red segments reverse, with a body outline drawn every few segments; then an amber car drives the path. "Show grid A*" overlays the path found by 8-connected search on a 1 m grid treating the car as a point (blue dashed): it demands turning on the spot at every corner, which the car cannot do. Hover the curvature limit in Section 5 to draw all primitives of one expansion at the start.
    • 选“死胡同”:街宽 9 m,掉头圆的直径就要约 9.7 m。“前进 + 倒车”时 Hybrid A* 找到一个多点掉头;改成“只前进”:找不到路。Choose "Dead end": the street is 9 m wide while a U-turn circle alone is about 9.7 m across. With "forward + reverse" Hybrid A* finds a multi-point turn; switch to "forward only": no path.
    • 选“长墙”,分别用两种启发式搜索:考虑障碍的启发式展开的节点不到直线距离的一半。Choose "Long wall" and search with each heuristic: the obstacle-aware heuristic expands fewer than half as many nodes as the straight-line one.

    几个细节。(1) 换成“状态格”:停车场仍能停进去,但要倒车调整两次,代价约为 Hybrid A* 的两倍;死胡同里要换向 5 次,代价 66.6,是 Hybrid A* 的 33.9 的近两倍;侧方停车干脆无从下手:目标位姿吸附到最近的整数格点 \((10,1)\) 后,车身压到了场地边界,读数显示“起点或目标与障碍相撞”。原因是 8 个航向、整数格点、4 m 长的转弯基元太粗了,换成更密的航向和更短的基元才行。基元集合的设计就是状态格规划器的全部功夫。(2) Hybrid A* 的路径还要后处理:实际系统会再做平滑(Dolgov 等用了带障碍项的共轭梯度优化,与 6.13、14.3 节的轨迹优化同类),再交给 9.1 节的路径跟踪控制器。(3) 打开“显示栅格 A*”比一比:点机器人的路径更短,却是车开不出的折线。

    A few details. (1) Switch to "State lattice": it still gets into the parking space, but with two reversing corrections at about twice the cost of Hybrid A*; in the dead end it needs 5 changes of direction at a cost of 66.6, nearly twice Hybrid A*'s 33.9; the parallel park cannot even start: snapped to the nearest integer lattice point \((10,1)\), the goal pose overlaps the edge of the site, and the readout reports that the start or goal pose collides. The reason is that 8 headings, integer lattice points and 4 m turning primitives are too coarse; denser headings and shorter primitives would be needed. Designing the primitive set is the whole craft of a lattice planner. (2) A Hybrid A* path still needs post-processing: real systems smooth it (Dolgov et al. used conjugate-gradient optimization with an obstacle term, the same family as the trajectory optimization of Lessons 6.13 and 14.3) before handing it to a path-tracking controller like those of Lesson 9.1. (3) Turn on "Show grid A*" and compare: the point-robot path is shorter, but it is a polyline the car cannot drive.

    边走边发现:重规划Discovering as you go: replanning

    下图的机器人一开始不知道墙在哪,只假设未知的地方都能走(乐观假设)。它每走一格,用半径为 \(r\) 的传感器看一圈,发现新墙就重规划。同一时刻用同样的已知地图做两件事:D* Lite 增量修补,以及从机器人当前位置从头跑一次 A*,比较两者展开的节点数。

    The robot below starts without knowing where the walls are and assumes every unknown cell is free (the optimistic assumption). After every step it looks around with a sensor of radius \(r\), and replans whenever it finds a new wall. At the same moments and on the same known map two things are done: D* Lite repairs incrementally, and A* runs from scratch from the robot's current cell; their expansion counts are compared.

    FIG 6未知地图里的重规划:D* Lite 对比每次从头 A*Replanning in an unknown map: D* Lite versus A* from scratch
    虚线方框是机器人还不知道的墙,实心格子是已经看到的墙。琥珀色圆点是机器人,青色圆是传感器范围,琥珀色线是它当前计划的路径,紫色线是走过的轨迹,G 是终点。最近一次重规划中,青色填充的格子是 D* Lite 处理过的节点,蓝色小点是从头 A* 展开的节点。点击地图可以添加或移除墙(机器人走近才会发现)。把鼠标放在上文的 \(\mathrm{rhs}\) 上,地图会用红色标出此刻 \(g\ne\mathrm{rhs}\)、还在队列里的节点。 Dashed squares are walls the robot does not know about yet; solid cells are walls it has seen. The amber dot is the robot, the cyan circle its sensor range, the amber line its current planned path, the purple line the track it has driven, and G the goal. In the latest replan, cells filled cyan are the nodes D* Lite processed and small blue dots the nodes A* expanded from scratch. Click on the map to add or remove walls (the robot finds out only when it comes close). Hover \(\mathrm{rhs}\) in the text above and the map marks in red the nodes with \(g\ne\mathrm{rhs}\) that are still in the queue.
    • 选“迷宫”,传感器半径保持 2 到 5(默认 3),按“运行”让机器人走到终点:D* Lite 的累计展开不到从头 A* 的一半。Choose "Maze", keep the sensor radius between 2 and 5 (default 3), and press "Run" until the robot reaches the goal: D* Lite's total expansions are less than half of A*-from-scratch's.

    在迷宫里(传感器半径 3),D* Lite 的累计展开只有从头 A* 的约三分之一(943 对 2767);半径越大,一次看到的墙越多,优势越小(半径 6 时约一半)。原因是:迷宫里直线距离启发式很差,从头 A* 每次都要重新填满大片走廊,而墙总是在机器人身边被发现,D* Lite 只需修补靠近机器人的一小块。在“房间”地图上差别就小得多,甚至可能反过来(半径 3 时 326 对 300):开阔地上 A* 本来就只展开一条窄带,D* Lite 每次修补还要付出维护 \(\mathrm{rhs}\) 和队列的开销。增量方法省多少,取决于变化发生在哪里、启发式有多好。还要注意第一次规划:迷宫里(半径 3)D* Lite 展开 350 个节点,A* 只展开 33 个。在还几乎全是未知(当作空闲)的地图上,许多格子的 \(f\) 相同;D* Lite 的第二段键取 \(g\) 小的先处理,会把这些平局的格子都处理一遍,而这里的 A* 平局时取 \(g\) 大的。D* Lite 的优势要在之后多次重规划里才体现出来。

    In the maze (sensor radius 3), D* Lite's total expansions are only about a third of A*-from-scratch's (943 versus 2767); a larger radius sees more walls at once and shrinks the advantage (about half at radius 6). The reason: the straight-line heuristic is poor in a maze, so every fresh A* has to refill large stretches of corridor, while walls are always discovered next to the robot and D* Lite only repairs a small patch near it. On the "Rooms" map the difference is much smaller and can even reverse (326 versus 300 at radius 3): in open space A* expands only a narrow band anyway, and every D* Lite repair also pays for maintaining \(\mathrm{rhs}\) and its queue. How much an incremental method saves depends on where the changes happen and how good the heuristic is. Note also the first plan: in the maze (radius 3) D* Lite expands 350 nodes and A* only 33. On a map that is still almost entirely unknown (treated as free), many cells share the same \(f\); D* Lite's second key component processes the smaller \(g\) first and so works through all those ties, whereas the A* here breaks ties toward the larger \(g\). D* Lite's advantage only shows over the many replans that follow.

    真实系统Real systems

    ROS 导航:ROS 1 的 navfn 与 global_planner 在代价地图(costmap,障碍按机器人半径“膨胀”,即 7.2 节的 C-障碍物)上跑 Dijkstra 或 A*;ROS 2 的 Nav2 提供 NavFn、Theta* 规划器,以及 Smac Planner 系列(二维 A*、Hybrid-A*、状态格三种)。自动驾驶:斯坦福的 Junior 在 2007 年 DARPA 城市挑战赛中用 Hybrid A* 在停车场和受阻路段规划(Dolgov 等);冠军 CMU 的 Boss 在停车场里用状态格加随时 D*(Likhachev 与 Ferguson)。今天的开源自动驾驶栈 Autoware(Autoware Universe)的泊车用 freespace planner 也基于 Hybrid A*。行星车:Stentz 的 D*(1994)为在部分未知地形中行驶的移动机器人而设计;Ferguson 与 Stentz 的插值版本 Field D* 于 2006 年随软件更新上传到 NASA 的火星探测漫游者(MER,勇气号与机遇号),在自主导航中与局部规划器 GESTALT 配合做全局规划(Carsten 等,2007)。游戏:A* 是游戏寻路的事实标准,在均匀栅格上常配合跳点搜索(JPS,Harabor 与 Grastien,2011)利用对称性跳过大量等价节点。

    ROS navigation: ROS 1's navfn and global_planner run Dijkstra or A* on a costmap (obstacles "inflated" by the robot radius, i.e. the C-obstacles of Lesson 7.2); ROS 2's Nav2 offers the NavFn and Theta* planners and the Smac Planner family (2D A*, Hybrid-A* and state lattice). Self-driving: Stanford's Junior used Hybrid A* for parking lots and blocked roads in the 2007 DARPA Urban Challenge (Dolgov et al.); the winner, CMU's Boss, used a state lattice with Anytime D* in parking lots (Likhachev and Ferguson). The parking freespace planner of the open-source Autoware stack (Autoware Universe) is also based on Hybrid A*. Planetary rovers: Stentz's D* (1994) was designed for mobile robots driving through partially known terrain; Ferguson and Stentz's interpolated version, Field D*, was uploaded to NASA's Mars Exploration Rovers (MER, Spirit and Opportunity) in a 2006 software update, where it does global planning alongside the local GESTALT planner during autonomous navigation (Carsten et al., 2007). Games: A* is the de facto standard for path-finding in games, on uniform grids often combined with Jump Point Search (JPS, Harabor and Grastien, 2011), which exploits symmetry to skip large numbers of equivalent nodes.

    接下来:图搜索在二维、三维里无可替代,但到了机械臂的 6、7 维关节空间就撑不住了。14.2 节改为在 \(\mathcal C\) 里随机撒点、用碰撞检测连线(PRM、RRT),14.3 节把图搜索或采样得到的折线优化成平滑、可执行的轨迹,15.3 节讲规划器所用的代价地图从哪里来。

    What comes next: graph search is irreplaceable in two and three dimensions, but it breaks down in the 6- or 7-dimensional joint space of an arm. Lesson 14.2 instead scatters random samples in \(\mathcal C\) and connects them with collision checks (PRM, RRT), Lesson 14.3 optimizes the polyline from search or sampling into a smooth, executable trajectory, and Lesson 15.3 covers where the planner's costmaps come from.

    7常见误区Common pitfalls

    误区 1:启发式与代价不是同一种“单位”Pitfall 1: a heuristic in different "units" from the cost

    可容许性是相对于你的代价函数说的。代价是“米”时直线距离可容许;代价若是“秒”,启发式就要除以最高速度;代价若是代价地图里的每格价格(例如 ROS 的 global_planner 每格收一个“中性代价”,默认 50,再加上代价地图值乘以 cost_factor,默认 3),最便宜的格子价格是多少,启发式就要乘多少。把曼哈顿距离用在 8 连通栅格上(FIG 3)、或者把格数当米用,都会悄悄高估,A* 依然“能跑”,只是不再保证最短。反过来,启发式比必要的小很多(比如价格最低是 50 却按 1 估计),A* 会退化得很接近 Dijkstra,慢但仍正确。

    Admissibility is relative to your cost function. If the cost is metres, the straight-line distance is admissible; if it is seconds, the heuristic must be divided by the top speed; if it is a costmap's per-cell price (ROS's global_planner, for example, charges a "neutral cost" per cell, 50 by default, plus the costmap value times cost_factor, 3 by default), the heuristic must be multiplied by the cheapest cell price. Manhattan on an 8-connected grid (FIG 3), or cells mistaken for metres, quietly overestimate: A* still "works" but no longer guarantees the shortest path. Conversely, a heuristic much smaller than necessary (cheapest price 50 but estimated at 1) makes A* degrade almost into Dijkstra: slow but still correct.

    误区 2:在入队时就把节点标记为“已访问”Pitfall 2: marking a node "visited" when it is pushed

    这是 BFS 的写法,用在 Dijkstra/A* 上就错了:第一次碰到某个节点时找到的路未必最便宜(FIG 1 里很多节点的 \(g\) 会被后来更新),只有取出时才确定。同类错误还有:用了二叉堆却不处理过期条目,同一节点被展开两次;启发式不一致却又不允许重开关闭的节点,结果悄悄失去最优性。还有一个效率细节:大片开阔区域里 \(f\) 相同的节点非常多,平局时优先 \(g\) 大的节点能少展开很多。

    That is how BFS is written, and it is wrong for Dijkstra/A*: the first route found to a node is not necessarily the cheapest (in FIG 1 many \(g\) values get improved later); a node is settled only when it is taken out. Related bugs: using a binary heap without handling stale entries, so one node is expanded twice; an inconsistent heuristic combined with no reopening of closed nodes, which silently loses optimality. One efficiency detail: in large open areas very many nodes share the same \(f\), and breaking ties toward the larger \(g\) saves many expansions.

    误区 3:把点机器人的栅格路径直接交给真实机器人Pitfall 3: handing a point robot's grid path straight to a real robot

    栅格上的最短路贴着障碍的角走,而真实机器人有半径:必须先把障碍按机器人半径膨胀(7.2 节的 C-障碍物),并且通常再加一层随距离衰减的代价,让路径离墙远一点。格子太粗,窄门会被“膨胀”成堵死;8 连通不禁止“斜穿墙角”,路径会从两个对角相接的障碍之间挤过去。拿到的路径还要平滑,对车来说还要满足最小转弯半径(FIG 5 的栅格 A* 对比,以及 9.1 节)。

    A grid shortest path hugs obstacle corners, but a real robot has a radius: obstacles must first be inflated by that radius (the C-obstacles of Lesson 7.2), and usually a cost decaying with distance is added on top to keep the path away from walls. With cells too coarse, a narrow door is "inflated" shut; an 8-connected search that does not forbid corner cutting squeezes between two obstacles touching at a corner. The resulting path still needs smoothing and, for a car, must respect the minimum turning radius (the grid A* comparison in FIG 5, and Lesson 9.1).

    误区 4:以为 Hybrid A* 是完备、最优的Pitfall 4: believing Hybrid A* is complete and optimal

    Hybrid A* 每个单元只保留一个连续状态,被丢掉的状态可能恰好是唯一可行的那个;它“找不到路”不等于真的没有路,代价也不保证最小(FIG 5 里换个启发式,路径代价都可能变)。航向分辨率太粗、基元太长时尤其明显。需要保证时,用状态格(对它这张图 A* 是最优的),或者把单元做细、把找不到路当作“需要更细分辨率”的信号而不是最终结论。

    Hybrid A* keeps one continuous state per cell, and a discarded state may be the only feasible one; "no path found" does not mean no path exists, and the cost is not guaranteed minimal (in FIG 5 even switching the heuristic can change the path cost). This shows especially with coarse heading resolution and long primitives. When guarantees matter, use a state lattice (A* is optimal on that graph), or refine the cells and treat "no path" as a signal to refine further rather than a final verdict.

    误区 5:把“未知”当成“空闲”或“占据”而不自知Pitfall 5: treating "unknown" as free or occupied without noticing

    FIG 6 用的是乐观假设:没看过的地方都当作能走。这让机器人敢于探索,但会一次次撞进死胡同再退出来;把未知当作占据则可能根本规划不出路。很多系统给未知格一个介于两者之间的代价,并在每个控制周期重规划。另外,增量算法不是免费的午餐:变化离终点近、或者一次变化很多时,D* Lite 可能比从头 A* 还慢,并且它的内存(每个节点的 \(g\)、\(\mathrm{rhs}\) 和队列)要一直保留。

    FIG 6 uses the optimistic assumption: anything not yet seen is treated as passable. That makes the robot willing to explore, but it repeatedly drives into dead ends and back out; treating unknown as occupied may leave no path at all. Many systems give unknown cells a cost in between and replan every control cycle. Also, incremental methods are not a free lunch: when changes are close to the goal, or many change at once, D* Lite can be slower than A* from scratch, and its memory (\(g\), \(\mathrm{rhs}\) and the queue for every node) must be kept around.

    8小练习Check yourself

    1. 8 连通栅格,直走一步代价 1,斜走一步代价 \(\sqrt2\),所有格子价格为 1。下面哪个启发式不可容许?1. An 8-connected grid where a straight step costs 1 and a diagonal step \(\sqrt2\), every cell price 1. Which heuristic is not admissible?
    对角相邻的格子:真实代价 \(\sqrt2\approx1.41\),曼哈顿距离却是 2,高估了。八方向距离在空地图上恰好等于真实代价,有障碍时只会更小;欧氏距离更小;\(h\equiv0\) 永远可容许(那就是 Dijkstra)。For diagonal neighbours the true cost is \(\sqrt2\approx1.41\) but Manhattan says 2: an overestimate. The octile distance equals the true cost on an empty map and can only be smaller than it with obstacles; Euclidean is smaller still; \(h\equiv0\) is always admissible (that is Dijkstra).
    2. 如果启发式恰好是真实剩余代价 \(h=h^*\),并且平局时优先 \(g\) 大的节点,A* 会展开哪些节点?2. If the heuristic is exactly the true remaining cost, \(h=h^*\), and ties go to the larger \(g\), which nodes does A* expand?
    最短路上的节点 \(f=g^*+h^*=C^*\),不在任何最短路上的节点 \(f>C^*\),后者永远不会在终点之前被取出。最短路上 \(f\) 都相等,“平局取 \(g\) 大”让搜索一路向前,不在最短路的分叉上停留。这说明完美的启发式就是价值函数(6.12 节的 \(V\)),而 A* 的工作量取决于 \(h\) 离它有多近。Nodes on a shortest path have \(f=g^*+h^*=C^*\); nodes on no shortest path have \(f>C^*\) and are never taken out before the goal. All \(f\) values along the shortest path are equal, and "ties to the larger \(g\)" keeps the search moving forward without lingering on side branches. The perfect heuristic is the value function (\(V\) of Lesson 6.12), and A*'s work depends on how close \(h\) is to it.
    3. 用一致启发式、\(w=1.5\) 的加权 A* 找到一条代价 30 的路径。关于最优代价 \(C^*\),能确定什么?3. Weighted A* with a consistent heuristic and \(w=1.5\) returns a path of cost 30. What can you say about the optimal cost \(C^*\)?
    上界“代价 \(\le w\,C^*\)”给出 \(C^*\ge30/1.5=20\);而 30 是一条真实路径的代价,最优代价不会比它大,所以 \(C^*\le30\)。FIG 3 里实际的代价比通常远低于 \(w\),所以真实的 \(C^*\) 往往接近 30。The bound "cost \(\le w\,C^*\)" gives \(C^*\ge30/1.5=20\); and 30 is the cost of an actual path, so the optimum cannot exceed it: \(C^*\le30\). In FIG 3 the actual ratio is usually far below \(w\), so the true \(C^*\) is often close to 30.
    4. Hybrid A* 为什么在节点里保存连续的 \((x,y,\theta)\),而不是把它对齐到单元中心?4. Why does Hybrid A* store a continuous \((x,y,\theta)\) in each node instead of snapping it to the cell centre?
    如果把位姿对齐到单元中心,基元的终点就不再是车真正能到达的位姿,拼起来的路径会有跳变。保存连续位姿让路径处处可行;但同一单元里只留一个状态,可能丢掉通往终点的那个,所以它既不完备也不最优。状态格走的是另一条路:把基元设计成正好落在格点上,于是可以对齐,最优性在格图上成立(选项 B 说的其实是状态格)。If poses were snapped to cell centres, a primitive's end would no longer be a pose the car actually reaches, and the stitched path would jump. Keeping the continuous pose makes the path feasible everywhere; but keeping only one state per cell may discard the one that leads to the goal, so the method is neither complete nor optimal. A state lattice takes the other road: primitives are designed to land exactly on lattice points, so snapping is exact and optimality holds on the lattice graph (option B really describes the state lattice).
    5. D* Lite 为什么从终点向机器人搜索,而不是像 A* 那样从机器人出发?5. Why does D* Lite search from the goal toward the robot instead of from the robot as A* does?
    如果从机器人出发,\(g\) 是“从机器人到这里”的代价,机器人一走全部作废。反向搜索让 \(g\) 以终点为基准,机器人的移动只改变“搜索要服务的起点”,靠 \(k_m\) 修正键值即可;新发现的障碍只让相邻节点的 \(\mathrm{rhs}\) 变化,修补局限在受影响的区域(FIG 6 的青色格子)。Searching from the robot, \(g\) would be the cost "from the robot to here", invalidated as soon as the robot moves. Searching backward anchors \(g\) at the goal; the robot's motion only changes which start the search must serve, handled by correcting keys with \(k_m\); a newly found obstacle changes only the \(\mathrm{rhs}\) of nearby nodes, so the repair stays inside the affected region (the cyan cells of FIG 6).

    9延伸阅读Further reading

    • E. W. Dijkstra, A note on two problems in connexion with graphs, Numerische Mathematik, 1959 — 最短路算法的原始论文,只有三页the original shortest-path paper, three pages long
    • P. E. Hart, N. J. Nilsson & B. Raphael, A Formal Basis for the Heuristic Determination of Minimum Cost Paths, IEEE Transactions on Systems Science and Cybernetics, 1968 — A* 及其最优性证明,为 Shakey 机器人而作A* and its optimality proof, written for the Shakey robot
    • I. Pohl, Heuristic search viewed as path finding in a graph, Artificial Intelligence, 1970 — 加权启发式搜索weighted heuristic search
    • M. Likhachev, G. Gordon & S. Thrun, ARA*: Anytime A* with Provable Bounds on Sub-Optimality, NIPS 2003 — 不重开时的 \(w\) 倍上界与随时改进the \(w\) bound without reopening, and anytime improvement
    • A. Stentz, Optimal and Efficient Path Planning for Partially-Known Environments, ICRA 1994 — D*D*
    • S. Koenig & M. Likhachev, D* Lite, AAAI 2002 — 本节 FIG 6 实现的算法the algorithm implemented in FIG 6
    • A. Nash, K. Daniel, S. Koenig & A. Felner, Theta*: Any-Angle Path Planning on Grids, AAAI 2007 — 任意角度路径any-angle paths
    • D. Dolgov, S. Thrun, M. Montemerlo & J. Diebel, Path Planning for Autonomous Vehicles in Unknown Semi-structured Environments, International Journal of Robotics Research, 2010 — Hybrid A*、两种启发式、解析扩展与路径平滑Hybrid A*, the two heuristics, analytic expansion and path smoothing
    • M. Pivtoraiko, R. A. Knepper & A. Kelly, Differentially Constrained Mobile Robot Motion Planning in State Lattices, Journal of Field Robotics, 2009 — 状态格state lattices
    • S. M. LaValle, Planning Algorithms, Cambridge University Press, 2006, Ch. 2 — 离散规划与图搜索的统一视角(网上免费)a unified view of discrete planning and graph search (free online)