← 目录 / 算法文档 · 模块十四 搜索进阶 / 14.2 双向 BFS

14.2 双向 BFS

起点和终点都已知时,与其让一条水波独自扩散到终点,不如让两端同时扩散,在中间碰头——能省下大量不必要的搜索。

本页目录
① 为什么需要双向 BFS:单向搜索的爆炸增长

14.1 节的 BFS 已经能保证找到最短路,但它有一个隐藏的代价:如果图的分支因子(平均每个节点能扩展到多少个新节点)是 b,最短路长度是 d,那么单向 BFS 在最坏情况下要访问的节点数量级是 O(b^d)——随着 d 增大,访问的节点数会指数级爆炸

如果终点也是已知的(比如"从状态 A 变到状态 B,最少几步",A 和 B 都给定),有一个更聪明的办法:从起点和终点同时开始扩展,两边各自只需要扩展到大约 d/2 层,就能在中间相遇。这样访问的节点数量级变成 O(2 × b^(d/2))——同样是指数级,但指数减半带来的差距非常悬殊:

分支因子 b最短路 d单向 BFS 访问量 O(b^d)双向 BFS 访问量 O(2×b^(d/2))
106约 1,000,000约 2,000
1010约 100 亿约 200,000
💡
为什么指数减半差距这么大?指数函数增长极快——b^d2×b^(d/2) 看起来只是指数少了一半,但因为是指数关系,一半的指数意味着底数相同时数值差了一个数量级以上的平方根倍。d 越大、b 越大,双向 BFS 相对单向 BFS 省下的计算量就越夸张。
② 核心思想:两端同时扩展,相遇即停

双向 BFS 维护两个独立的 BFS:一个从起点 S 出发向外扩展,一个从终点 T 出发向外扩展。两边各自维护自己的距离数组(distSdistT),交替扩展一层。每次扩展新节点时,检查这个节点有没有被另一边访问过——一旦发现某个节点同时被两边访问到,说明两股"水波"在这里相遇了,答案就是 distS[相遇点] + distT[相遇点]

一条长度为 4 的路径上,两种 BFS 的访问范围对比
单向 BFS:从 S 一路扩展到 T,要访问全部 5 个节点
S
dist=0
A
dist=1
B
dist=2
C
dist=3
T
dist=4
双向 BFS:两端各自只扩展 2 层,在 B 相遇
S
distS=0
A
distS=1
B
distS=2 distT=2
C
distT=1
T
distT=0
相遇点 B 同时被"从 S 出发"和"从 T 出发"的两股搜索访问到:distS[B]=2distT[B]=2,答案 = distS[B] + distT[B] = 4,和单向 BFS 算出的路径长度一致,但双向 BFS 全程只碰到了 S、A、B、C、T 中的每一个节点各一次,在真实的大规模图上(分支因子较大时)能少访问大量根本没必要看的节点。
③ 模板代码

以一般图(邻接表)为例,两个方向交替各扩展一整层,每扩展一个新节点就检查它是否已经被对面访问过:

C++ · 双向 BFS 模板
1vector<int> adj[MAXN];
2int distS[MAXN], distT[MAXN]; // -1 表示未访问;分别记录"从起点/终点出发"的距离
3
4// 扩展 q 队列一整层;hitDist 是"对面"的距离数组,用来检测相遇
5int ExpandLayer(queue<int> &q, int myDist[], int hitDist[])
6{
7 int sz = q.size(); // 只扩展"当前已经在队列里"的这一层,不把新加入的也算进去
8 for (int t = 0; t < sz; t++)
9 {
10 int u = q.front(); q.pop();
11 for (int v : adj[u])
12 {
13 if (hitDist[v] != -1) return myDist[u] + 1 + hitDist[v]; // ★ 对面已经到过 v,相遇!
14 if (myDist[v] != -1) continue; // 自己这边已经访问过,跳过
15 myDist[v] = myDist[u] + 1;
16 q.push(v);
17 }
18 }
19 return -1; // 这一层扩展完还没相遇
20}
21
22int BidirectionalBFS(int S, int T)
23{
24 if (S == T) return 0;
25 memset(distS, -1, sizeof(distS));
26 memset(distT, -1, sizeof(distT));
27 queue<int> qS, qT;
28 distS[S] = 0; qS.push(S);
29 distT[T] = 0; qT.push(T);
30
31 while (!qS.empty() && !qT.empty())
32 {
33 int res = ExpandLayer(qS, distS, distT); // 从起点这边扩展一层
34 if (res != -1) return res;
35 res = ExpandLayer(qT, distT, distS); // 从终点这边扩展一层
36 if (res != -1) return res;
37 }
38 return -1; // 两边都扩展完了还没相遇,说明 S、T 不连通
39}
📌
和普通 BFS 相比多了什么?结构上几乎一样,只是从"一个队列、一个距离数组"变成了"两个队列、两个距离数组",并且第 7 行用 q.size() 提前记录本层节点数,确保每次只扩展"已经在队列里"的这一层,不会把这一层扩展时新塞进去的下一层节点也算进当前这一轮——这样才能保证两边真正是"交替地一层一层"扩展,而不是一边偷跑到很深的地方。
④ 图解:两个方向的队列如何在中间相遇

用一张网格图演示:起点 S 在左上角,终点 T 在右下角,两支队列交替扩展,直到某一层扩展时踩到对方的地盘:

两支队列交替扩展的过程
起点队列
第 1 层
S 的邻居们
distS 记为 1;此时 distT 里没有任何一个是这些点,未相遇
终点队列
第 1 层
T 的邻居们
distT 记为 1;同样未相遇
起点队列
第 2 层
M
扩展到节点 M 时,发现 distT[M] != -1(终点那边第 1 层已经到过 M)——相遇!直接返回 distS[M] + 1 + distT[M]
⑤ 适用条件与常见陷阱

双向 BFS 不是任何时候都能用,也不是任何时候都比单向 BFS 划算:

终点必须提前已知:双向 BFS 的前提是"起点和终点都给定,求两者之间的最短距离"。如果问题是"从起点出发,找到第一个满足某种条件的位置"(终点本身不确定,需要一边搜索一边判断),就没法从终点反向扩展,只能用普通的单向 BFS。
忘记检查"扩展到的新节点"和"相遇判断"的先后顺序:第 13、14 行的判断顺序很关键——必须先检查对面有没有访问过(判断相遇),再检查自己有没有访问过(避免重复入队)。如果顺序反了,可能会在已经相遇的情况下继续把节点当作"自己的新节点"入队,导致要么错过正确答案,要么多做无用功。
有向图中,终点这边不能沿着原方向扩展:如果图是有向图(边有方向),从终点 T 出发反向扩展时,要沿着反向边走("谁能到达 T"而不是"T 能到达谁"),需要额外建一张反图。本节的例子和模板代码都以无向图为例,实际做有向图的双向 BFS 时要特别注意这一点。
🏆
本节小结:双向 BFS 本质仍然是 BFS——用队列保证"先近后远",只是同时从两端各跑一遍,利用"指数减半"的数学优势大幅压缩要访问的节点数。它不能替代单向 BFS(终点未知时用不了),但在终点已知、分支因子较大的场景(比如经典的"八数码"问题两端状态之间求最少步数)非常有效,是竞赛中常见的 BFS 优化技巧之一。