来源:算法讲解063【必备】双向广搜 · 2026-10-28
依据:视频字幕 2000 字
双向广搜(Bidirectional BFS)是从起点和终点同时做宽度优先搜索(BFS),每次只扩展当前队列更小的一侧,两侧在中间相遇时即得最短距离;核心价值是把搜索空间从指数级压到约平方根级。
unordered_map 或手写哈希。设 S 到 E 的最短距离为 d。
以「求 S 到 E 的最短步数」为例,状态用可哈希类型表示:
S == E,直接返回 0。qS, qE,两个哈希表 distS, distE(记录状态 → 层数)。distS[S]=0, distE[E]=0,各自入队。 1. 选 qS.size() <= qE.size() 的那一侧作为「当前扩展侧」。
2. 记下当前侧本层的节点数 sz = 当前队列大小(只扩这一层,保证层数正确)。
3. 对 sz 个节点逐个弹出,枚举所有一步转移得到 nxt:
- 若 nxt 已在本侧 dist 中,跳过(判重)。
- 记录本侧距离 = 当前层 + 1,入队。
- 若 nxt 在另一侧 dist 中出现过,说明相遇,用 dist本侧[nxt] + dist另一侧[nxt] 更新答案(注意:不要立刻 return,见易错点;若题目保证第一次相遇即最优且你按层扩,也可直接返回,但要写对)。
4. 本层扩完后,若答案已被更新,可返回(按层扩展下,本层产生的候选已是最优层数组合)。
关键点:每次只扩一层,且扩的是「队列更小」的那一侧;两侧距离表分开维护。
图:S - a - b - E,另有一条长链 S - c - d - f - g - E(边权均为 1)。真实最短路 S→a→b→E,长度 3。
初始:qS=[S] distS{S:0},qE=[E] distE{E:0}。
| 轮次 | 选侧(队列小) | 扩展层 | 新访问 | 相遇? | 答案 |
|---|---|---|---|---|---|
| 1 | qS(1) vs qE(1),选 S | S→{a,c} | distS{a:1,c:1} | 无 | — |
| 2 | qS(2) vs qE(1),选 E | E→{b,g} | distE{b:1,g:1} | 无 | — |
| 3 | qS(2) vs qE(2),选 S | a→{b}, c→{d} | distS{b:2,d:2} | b 在 distE 中! | 2+1=3 |
| 4 | 本层扩完,返回 | | | | 3 |
对比单向 BFS:从 S 出发要扩到第 3 层才碰到 E,中间会把 c,d,f,g 全展开;双向只扩了 2 层就相遇。
from collections import deque
def bidirectional_bfs(start, end, neighbors):
"""neighbors(state) -> 可迭代的一步可达状态集合"""
if start == end:
return 0
qs, qe = deque([start]), deque([end])
ds, de = {start: 0}, {end: 0}
while qs and qe:
# 每次扩队列更小的一侧
if len(qs) <= len(qe):
q, d_this, d_other = qs, ds, de
else:
q, d_this, d_other = qe, de, ds
sz = len(q) # 只扩本层
for _ in range(sz):
cur = q.popleft()
for nxt in neighbors(cur):
if nxt in d_this:
continue
d_this[nxt] = d_this[cur] + 1
if nxt in d_other: # 相遇
return d_this[nxt] + d_other[nxt]
q.append(nxt)
return -1
说明:上面「一相遇就返回」在按层扩展且两侧都只扩一层的前提下是正确的(第一次相遇的层数和最小)。若你的实现不是严格按层(比如一次扩多个节点但没锁层),必须改成「本层扩完再取最小」,见易错点。
#include <bits/stdc++.h>
using namespace std;
// 单词接龙:每次改一个字符,字符集为小写字母;返回最短步数,不可达返回 0
// 数据规模:单词长度 <= 10,词表 <= 5000(典型 LC 127 规模)
int ladderLength(const string& beginWord, const string& endWord,
const vector<string>& wordList) {
unordered_set<string> dict(wordList.begin(), wordList.end());
if (!dict.count(endWord)) return 0;
if (beginWord == endWord) return 0;
unordered_map<string, int> ds, de;
queue<string> qs, qe;
ds[beginWord] = 0; qs.push(beginWord);
de[endWord] = 0; qe.push(endWord);
auto expand = [&](queue<string>& q, unordered_map<string, int>& dthis,
unordered_map<string, int>& dother) -> int {
int sz = (int)q.size(); // 只扩本层
for (int i = 0; i < sz; ++i) {
string cur = q.front(); q.pop();
string nxt = cur;
for (char& c : nxt) {
char old = c;
for (char ch = 'a'; ch <= 'z'; ++ch) {
if (ch == old) continue;
c = ch;
if (!dict.count(nxt)) continue;
if (dthis.count(nxt)) continue;
dthis[nxt] = dthis[cur] + 1;
if (dother.count(nxt))
return dthis[nxt] + dother[nxt];
q.push(nxt);
}
c = old;
}
}
return -1; // 本层未相遇
};
while (!qs.empty() && !qe.empty()) {
int ans;
if (qs.size() <= qe.size())
ans = expand(qs, ds, de);
else
ans = expand(qe, de, ds);
if (ans != -1) return ans;
}
return 0;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// 示例
vector<string> wl = {"hot","dot","dog","lot","log","cog"};
cout << ladderLength("hit", "cog", wl) << "\n"; // 5
return 0;
}
unordered_map 在 1e5~1e6 状态量下建议 reserve 或用 gp_hash_table/手写哈希;字符串状态建议先编码成整数(如把单词映射成 id)再搜,能显著降常数。#include <bits/stdc++.h>
using namespace std;
// 通用双向 BFS 骨架:状态用 int 编码,neighbors 返回一步可达状态
// distS/distE 用 unordered_map 或(状态范围小时)数组
int bidirectionalBFS(int start, int target,
const function<vector<int>(int)>& neighbors) {
if (start == target) return 0;
unordered_map<int,int> distS, distE;
queue<int> qS, qE;
distS[start] = 0; qS.push(start);
distE[target] = 0; qE.push(target);
auto expand = [&](queue<int>& q, unordered_map<int,int>& dThis,
unordered_map<int,int>& dOther) -> int {
int sz = (int)q.size();
for (int i = 0; i < sz; ++i) {
int cur = q.front(); q.pop();
for (int nxt : neighbors(cur)) {
if (dThis.count(nxt)) continue;
dThis[nxt] = dThis[cur] + 1;
if (dOther.count(nxt)) return dThis[nxt] + dOther[nxt];
q.push(nxt);
}
}
return -1;
};
while (!qS.empty() && !qE.empty()) {
int ans = (qS.size() <= qE.size())
? expand(qS, distS, distE)
: expand(qE, distE, distS);
if (ans != -1) return ans;
}
return -1; // 不可达
}
sz = q.size() 后只弹 sz 个。distS+distE 不一定最小,应在本层扩完后取所有候选的最小值。start == end 没特判:有的题要求返回 0,有的返回 1(如单词接龙若 begin==end 且合法),按题意。endWord 不在词表直接无解,先判。unordered_map 被卡:出题人可能构造哈希冲突,竞赛里对状态量大的题用 reserve 或换 map/手写哈希/离散化后数组。long long 或 pair 哈希。while(!qS.empty() && !qE.empty()),一侧空了就无解。distS+distE 等于最短的路径数,需在本层扩完后统计,不能提前返回。list/map 存队列和距离表,常数爆炸,1e5 以上直接 TLE;用 vector/queue/unordered_map(带 reserve)。ios::sync_with_stdio(false); cin.tie(nullptr);,大数据输入慢。题 1:单词接龙,begin="hit", end="cog", wordList=["hot","dot","dog","lot","log","cog"],求最短转换序列长度(含首尾单词数)。
5(hit→hot→dot→dog→cog)。双向 BFS:S 侧扩 hit→hot,E 侧扩 cog→dog/log,再扩 hot→dot/lot,dot→dog 与 E 侧 dog 相遇,距离 2+2=4 步,序列长度 5。
</details>
题 2:判断:双向 BFS 中「每次扩展队列更小的一侧」是为了保证正确性。
错。正确性由「无向图 + 按层扩展 + 两侧距离表」保证;扩小侧只是优化(让两侧深度均衡,减少总扩展量),不影响正确性。
</details>
题 3:八数码从初始状态到目标状态,单向 BFS 状态数约 9!/2 ≈ 18 万,看似可行;但若棋盘变成 4×4 的十五数码,状态数约 16!/2 ≈ 1e13,单向不可行。问:能否用双向 BFS?为什么?需要注意什么?
能。十五数码操作可逆(移动空格可逆),图无向,满足双向前提;双向只需展开约 sqrt(1e13) ≈ 3e6 量级状态,可行。注意:状态编码要紧凑(如 4 位/格压成 64 位整数或康托展开),哈希表要 reserve,按层扩展、本层扩完再判相遇,多测清空。
</details>