专栏文章
专栏文章
地图导航算法系列
1. 地图导航算法 #01:A* 算法 2. 地图导航算法 #02:双向搜索 3. 地图导航算法 #03:Contraction Hierarchies 4. 地图导航算法 #04:Hub Labeling 5. 地图导航算法 #05:Customizable Contraction Hierarchies

地图导航算法 #02:双向搜索

发布于 2026-06-08 09:03 👁 10 次阅读

双向搜索同时从起点和终点向中间展开,两个搜索前沿相遇时即可合并路径;理论上将搜索空间从半径 r 的球体压缩为两个半径 r/2 的球体,节点展开量减少约 50%,是 Contraction Hierarchies 查询阶段的核心机制。

相关文章A* 算法 · Contraction Hierarchies

bidirectional search

目录

章节 说明
核心思想 两端同时搜索,搜索量减半
双向 Dijkstra 算法流程与终止条件(易错点)
双向 A* 启发函数需要一致性修正
终止条件的陷阱 为什么不能简单地在相遇时停止
Java 实现 双向 Dijkstra 完整代码
性能与局限 实际加速比及适用边界
参考资料 论文与文档

核心思想

单向搜索以起点为圆心,半径 r 扩展,搜索空间 ∝ r²(二维)或 r³(三维)。

双向搜索以起点和终点各扩展半径 r/2,两个球体相遇:

搜索空间(简化为二维圆面积):

  单向:π × r²          双向:2 × π × (r/2)² = π × r² / 2

  理论节点减少:50%

实际路网上加速比通常在 2x~4x,配合 A* 启发函数可进一步提升。


双向 Dijkstra

数据结构

维护两套独立的状态:

变量 前向(从 S 出发) 后向(从 G 出发)
优先队列 fwdQ(按 distF 排序) bwdQ(按 distB 排序)
距离数组 distF[v] distB[v]
已探索集 closedF closedB

后向搜索在反向图上运行(原图所有边反转)。

算法流程

初始化:
  distF[S] = 0,  distB[G] = 0
  fwdQ = {S},    bwdQ = {G}
  best = ∞,  meetNode = -1

交替展开(每轮选 f 值更小的队列展开一步):
  从 fwdQ 取出 u(distF 最小):
    for 每条边 u→v(正向图):
      if distF[u] + w(u,v) < distF[v]:
        更新 distF[v],入队
    if v ∈ closedB:                  ← v 已被后向探索
      candidate = distF[v] + distB[v]
      if candidate < best:
        best = candidate, meetNode = v

  从 bwdQ 取出 u(distB 最小):
    (对称处理反向图)

终止条件(见下节):
  当 distF[u] + distB[u] ≥ best 时停止

路径重建

从 meetNode 向前回溯 parentF → 得到 S→meetNode 的路径
从 meetNode 向后回溯 parentB → 得到 meetNode→G 的路径
拼接两段路径

双向 A*

双向 A* 比双向 Dijkstra 更复杂,因为两个方向的启发函数需要保持一致性

启发函数修正(Pohl 修正)

前向启发函数 hF(v) 估计 v→G 的代价,后向启发函数 hB(v) 估计 v→S 的代价。

直接使用两个独立启发函数会导致搜索前沿不对称,可能错过最优解。Pohl(1971)提出修正:

p(v) = [ hF(v) - hB(v) ] / 2

前向优先级:f_fwd(v) = distF[v] + p(v) + C/2
后向优先级:f_bwd(v) = distB[v] - p(v) + C/2

C = hF(S)(起点到终点的初始启发值,常数)

实践中,地图导航常用双向 A* 近似版本:直接用欧式距离作为两个方向的启发函数,接受极少概率的次优解,换取实现简洁性。


终止条件的陷阱

错误的终止条件

// ❌ 错误:两个前沿有公共节点就停止
if (closedF.contains(u) && closedB.contains(u)) return;

反例

S ──1── A ──100── G
S ──50── B ──1── G

前向展开 S→A(代价 1),后向展开 G→A(代价 100),A 进入两个 closed 集合。 若此时停止,返回路径 S→A→G(代价 101),但最优路径是 S→B→G(代价 51)。

正确的终止条件

维护全局最优值 best。当两个队首的代价之和 ≥ best 时,才能安全停止:

// ✅ 正确:队首 d_fwd + d_bwd ≥ 当前最优才终止
double fwdTop = fwdQ.peek().dist;
double bwdTop = bwdQ.peek().dist;
if (fwdTop + bwdTop >= best) break;

每次从任一方向松弛到节点 v 时,若 v 已被另一方向探索,更新 best

if (closedB.contains(v)) {
    best = Math.min(best, distF[v] + distB[v]);
}

Java 实现

import java.util.*;

public class BidirectionalDijkstra {

    record State(int node, double dist) implements Comparable<State> {
        public int compareTo(State o) { return Double.compare(dist, o.dist); }
    }

    /**
     * @param fwd  正向邻接表:fwd[u] = [(v, weight)]
     * @param bwd  反向邻接表:bwd[v] = [(u, weight)](即原图边反转)
     */
    public static double search(List<double[]>[] fwd, List<double[]>[] bwd, int S, int G) {
        int n = fwd.length;
        double[] dF = new double[n], dB = new double[n];
        Arrays.fill(dF, Double.MAX_VALUE);
        Arrays.fill(dB, Double.MAX_VALUE);
        boolean[] closedF = new boolean[n], closedB = new boolean[n];

        dF[S] = 0; dB[G] = 0;
        PriorityQueue<State> qF = new PriorityQueue<>(), qB = new PriorityQueue<>();
        qF.offer(new State(S, 0)); qB.offer(new State(G, 0));

        double best = Double.MAX_VALUE;

        while (!qF.isEmpty() || !qB.isEmpty()) {
            // 终止条件:两个队首之和 >= 当前最优
            double topF = qF.isEmpty() ? Double.MAX_VALUE : qF.peek().dist;
            double topB = qB.isEmpty() ? Double.MAX_VALUE : qB.peek().dist;
            if (topF + topB >= best) break;

            // 选代价更小的方向展开一步
            if (topF <= topB) {
                State cur = qF.poll();
                if (closedF[cur.node]) continue;
                closedF[cur.node] = true;
                for (double[] e : fwd[cur.node]) {
                    int v = (int)e[0]; double w = e[1];
                    if (dF[cur.node] + w < dF[v]) {
                        dF[v] = dF[cur.node] + w;
                        qF.offer(new State(v, dF[v]));
                        if (closedB[v])
                            best = Math.min(best, dF[v] + dB[v]);  // 更新最优
                    }
                }
            } else {
                State cur = qB.poll();
                if (closedB[cur.node]) continue;
                closedB[cur.node] = true;
                for (double[] e : bwd[cur.node]) {
                    int v = (int)e[0]; double w = e[1];
                    if (dB[cur.node] + w < dB[v]) {
                        dB[v] = dB[cur.node] + w;
                        qB.offer(new State(v, dB[v]));
                        if (closedF[v])
                            best = Math.min(best, dF[v] + dB[v]);
                    }
                }
            }
        }
        return best == Double.MAX_VALUE ? -1 : best;
    }
}

性能与局限

实际加速比

场景 理论加速 实测(道路网络)
双向 Dijkstra 2x 1.5x~3x
双向 A* 3x~5x 2x~4x

实测低于理论值,原因:路网并非均匀球体,城郊道路密度差异导致两个前沿扩展速度不对称。

局限

在 Contraction Hierarchies 中的角色

CH 查询阶段就是一种特殊的双向搜索——但只在预处理好的层级图上运行,搜索空间被压缩到数千个节点,单次查询 < 1ms。双向搜索是 CH 的内核,而不是替代品。

详见 Contraction Hierarchies。


参考资料

参考资料

  • Pohl, Bi-directional search (1971) — 双向 A* 启发函数修正
  • A* 算法(单向启发式搜索基础)
  • Contraction Hierarchies(工业级导航算法,双向搜索的升级版)
← 返回列表

评论 (0)

暂无评论,来留下第一条吧。

发表评论