Finding the shortest path between two specific vertices s and t in a large graph with a single BFS from s expands every vertex within distance d of the source. That frontier grows as b^d, where b is the branching factor and d is the shortest-path length — for b = 10, d = 6 that is on the order of a million vertices, almost all of them nowhere near t.

Bidirectional search launches a second BFS backward from t over reversed edges and runs both at once. Each side only has to reach depth d/2 before the two frontiers collide somewhere in the middle, so together they expand about 2·b^(d/2) vertices — roughly two thousand for the same b = 10, d = 6. Two small frontiers growing toward each other sweep far less of the graph than one frontier covering the whole distance.

The saving is conditional. The target has to be a concrete vertex the backward search can start from, and the graph has to expose predecessors — a reverse adjacency list, or an invertible move operator for an implicit state space. Without both, the second frontier has nothing to grow from.

Core condition: one known target + enumerable predecessors → forward and backward BFS meet near depth d/2O(b^(d/2)) time and space instead of O(b^d).

Visualization pending

Planned StepTrace: a two-frontier graph card showing forward and backward BFS expanding outward from source and target until they first intersect at a meeting vertex, then the path spliced through it. No matching renderer exists in engine.js yet.

Meeting in the middle

Two frontiers advance in parallel. F holds the vertices reached from s along forward edges; B holds the vertices reached from t along reversed edges. Each side records the distance at which it first reached every vertex.

Take an unweighted graph where s and t are six edges apart. The forward BFS reaches depths 0, 1, 2, 3 from s; the backward BFS reaches depths 0, 1, 2, 3 from t. The two sides collide at a vertex three edges from each end, and neither BFS ever expands a fourth level. With branching factor b, the forward side holds about b^3 vertices and the backward side about b^3, against the b^6 a single BFS would expand to reach depth six. Splitting one path of length d into two halves is what caps each search at depth d/2.

For an unweighted graph the stopping rule is exact. Expansion proceeds one full BFS level at a time, alternating sides, and the search halts the first time a level completes with a vertex present in both visited sets. Because each side labels every vertex with its true BFS distance, a shared vertex x lies on a path of length distF[x] + distB[x]; scanning that first overlapping level for the smallest such sum yields a shortest path. The path is rebuilt by following forward parents from x back to s, reversing, then appending the backward parents from x to t.

Expanding whichever frontier currently holds fewer vertices keeps the two searches near the same depth. A lopsided pair — one side pushed far past the other — loses the halving, because the deeper frontier is already climbing back toward b^d.

Complexity

CaseTimeAuxiliary spaceCause
BestO(b)O(b)s and t sit a few edges apart; the frontiers intersect after a handful of expansions.
AverageO(b^(d/2))O(b^(d/2))The frontiers meet near depth d/2; each side expands and stores about b^(d/2) vertices.
WorstO(b^(d/2))O(b^(d/2))The meeting sits at full depth or the shortest path just reaches length d; both frontiers grow to maximum size before touching.

† A connected worst case stays at O(b^(d/2)), but disconnected endpoints are worse: both searches exhaust their entire reachable sets, O(V + E), before reporting that no path exists.

Here b is the branching factor and d the shortest-path length. The bounds assume a roughly uniform graph where every vertex has about b successors and b predecessors and both sides advance in balance. Space matches time because both frontiers plus their distance maps must be held in memory to test for intersection — there is no O(1)-space variant, unlike unidirectional search. The unidirectional BFS baseline on the same input is O(b^d) time and space; bidirectional search halves the exponent, not the constant.

Where the clean case ends

Weighted edges break first-touch. Once the frontiers advance by cumulative cost rather than by level, the first vertex to appear in both visited sets is no longer guaranteed shortest — a cheaper meeting can still be one relaxation away at a different vertex whose two halves are not both settled. A correct termination tracks the best summed cost μ = min(gF[u] + gB[u]) over all met vertices and keeps expanding until the sum of the two frontiers’ smallest keys — their current search radii — reaches μ. Only then can no unexpanded path undercut the best meeting. Returning on first contact is the classic bidirectional-search correctness bug; unweighted BFS avoids it because level-order expansion settles distances in nondecreasing order, so the first overlap is already optimal.

The target must be a concrete vertex. The backward search needs somewhere to start, so a goal defined only by a predicate — “any solved state”, “any node with property P” — leaves nothing to grow a backward frontier from, and the meet-in-the-middle mechanism does not apply. A unidirectional search that expands forward until the predicate holds is the only option there.

Predecessors must be enumerable. The backward BFS walks edges in reverse, so a directed graph needs a reverse adjacency list and an implicit state space needs an invertible move operator. Without incoming edges the backward frontier cannot advance past depth zero, and the b^(d/2) bound depends on that frontier reaching depth d/2. Undirected graphs supply this for free.

Reference drawer

Questions

References