10.12 Accelerating Searches with Bidirectional Search

Many shortest-path algorithms begin at the source and gradually expand outward until the target is reached.

10.12 Accelerating Searches with Bidirectional Search

Many shortest-path algorithms begin at the source and gradually expand outward until the target is reached. This works well, but it often explores far more of the graph than necessary.

Consider finding a route between two cities on opposite sides of a large road network. A traditional search expands a growing region from the source. Much of that exploration occurs far from the destination.

An alternative strategy is surprisingly simple:

  • Search forward from the source.
  • Search backward from the destination.
  • Stop when the two searches meet.

This technique is called bidirectional search.

When applicable, bidirectional search can reduce exploration dramatically and often provides one of the largest practical speedups available for shortest-path problems.

Problem

Given a source vertex and a target vertex in an unweighted graph, find the shortest path while minimizing the number of explored vertices.

Consider:

A - B - C - D - E - F - G

A traditional BFS starting at A explores:

A
A B
A B C
A B C D
A B C D E
A B C D E F
A B C D E F G

Most of the graph is visited.

A bidirectional search expands from both ends:

A -> B -> C -> D
G -> F -> E -> D

The searches meet near the middle.

Solution

Maintain two BFS frontiers:

  • Forward search from the source
  • Reverse search from the target

Terminate when a vertex is discovered by both searches.

package main

import "fmt"

func bidirectionalBFS(
	graph [][]int,
	source int,
	target int,
) int {

	if source == target {
		return 0
	}

	n := len(graph)

	distFromSource := make([]int, n)
	distFromTarget := make([]int, n)

	for i := 0; i < n; i++ {
		distFromSource[i] = -1
		distFromTarget[i] = -1
	}

	queueSource := []int{source}
	queueTarget := []int{target}

	distFromSource[source] = 0
	distFromTarget[target] = 0

	headSource := 0
	headTarget := 0

	for headSource < len(queueSource) &&
		headTarget < len(queueTarget) {

		if headSource < len(queueSource) {
			v := queueSource[headSource]
			headSource++

			for _, next := range graph[v] {
				if distFromSource[next] != -1 {
					continue
				}

				distFromSource[next] =
					distFromSource[v] + 1

				queueSource =
					append(queueSource, next)

				if distFromTarget[next] != -1 {
					return distFromSource[next] +
						distFromTarget[next]
				}
			}
		}

		if headTarget < len(queueTarget) {
			v := queueTarget[headTarget]
			headTarget++

			for _, next := range graph[v] {
				if distFromTarget[next] != -1 {
					continue
				}

				distFromTarget[next] =
					distFromTarget[v] + 1

				queueTarget =
					append(queueTarget, next)

				if distFromSource[next] != -1 {
					return distFromSource[next] +
						distFromTarget[next]
				}
			}
		}
	}

	return -1
}

func main() {
	graph := [][]int{
		{1},
		{0, 2},
		{1, 3},
		{2, 4},
		{3, 5},
		{4, 6},
		{5},
	}

	fmt.Println(
		bidirectionalBFS(graph, 0, 6),
	)
}

Output:

6

The shortest path contains six edges.

Why Bidirectional Search Works

Ordinary BFS explores vertices in increasing distance order.

Suppose the shortest path length is:

d

A standard search explores roughly all vertices within distance:

d

of the source.

Bidirectional search instead explores:

d / 2

from each side.

This may appear like a small change.

It is not.

Understanding the Branching Factor

Suppose each vertex has:

b

neighbors on average.

A traditional BFS explores approximately:

1 + b + b² + ... + bᵈ

vertices.

This grows approximately as:

bᵈ

A bidirectional search explores:

2 × (1 + b + b² + ... + b^(d/2))

which grows approximately as:

2b^(d/2)

The difference is enormous.

For example:

Branching Factor Distance BFS Bidirectional
10 6 1,000,000 2,000
10 8 100,000,000 20,000
10 10 10,000,000,000 200,000

This exponential reduction explains the popularity of bidirectional search.

Traditional BFS:

          S
       / /|\ \\n      / / | \ \\n     / /  |  \ \\n    ... expanding ...

The frontier grows outward continuously.

Bidirectional BFS:

S ---> ---> <--- <--- G

The searches meet near the center.

Only a fraction of the graph must be explored.

Detecting the Meeting Point

The algorithm maintains two distance arrays.

distFromSource[]
distFromTarget[]

Whenever a vertex becomes known to both searches:

if distFromTarget[next] != -1

or

if distFromSource[next] != -1

the shortest path has been discovered.

Its length is:

distance from source
+
distance from target

The meeting vertex acts as the connection point.

Path Reconstruction

Distances provide the path length.

To reconstruct the actual route, store predecessor arrays for both searches.

parentSource[]
parentTarget[]

When the searches meet:

source -> meeting vertex

is reconstructed using:

parentSource

and

meeting vertex -> target

is reconstructed using:

parentTarget

The two halves are concatenated.

This produces the complete shortest path.

Bidirectional Dijkstra

The same idea extends to weighted graphs.

Run:

  • Dijkstra from the source
  • Dijkstra from the target

Maintain two priority queues.

The searches stop when the best possible future path can no longer improve the current answer.

The implementation is significantly more complex than bidirectional BFS, but the underlying idea remains identical.

Large road-navigation systems frequently employ bidirectional variants.

Reverse Graphs

For directed graphs, the backward search cannot simply follow outgoing edges.

Instead, construct a reverse graph.

If:

A -> B

exists in the original graph, then:

B -> A

exists in the reverse graph.

The forward search uses the original graph.

The backward search uses the reversed graph.

This allows both searches to move toward each other correctly.

When Bidirectional Search Helps

The technique works best when:

  • Source and target are known.
  • The graph is large.
  • The branching factor is significant.
  • A single shortest path is needed.

Examples include:

  • Navigation systems
  • Social-network path finding
  • State-space search
  • Puzzle solving
  • Route planning

The larger the graph, the greater the benefit.

When Bidirectional Search Does Not Help

If shortest paths to every vertex are required, bidirectional search is usually inappropriate.

For example:

Single-source shortest paths
All-pairs shortest paths
Reachability analysis

remain better suited to BFS, Dijkstra, Bellman-Ford, or Floyd-Warshall.

Bidirectional search is primarily a source-to-target optimization.

Discussion

The most important lesson is that shortest-path performance often depends more on search space reduction than on data-structure optimization.

Replacing a heap with a faster heap may improve performance by a constant factor.

Reducing the explored search space from:

bᵈ

to:

2b^(d/2)

changes the problem fundamentally.

This distinction appears repeatedly in algorithm design.

The largest gains often come from avoiding work rather than performing work more efficiently.

Complexity

Worst-case complexity remains similar to ordinary BFS.

Operation Complexity
Forward BFS O(V + E)
Reverse BFS O(V + E)
Total worst case O(V + E)
Memory usage O(V)

The theoretical bound does not change.

The practical number of explored vertices often decreases dramatically.

Common Mistakes

Do not terminate as soon as the two frontiers touch. The meeting condition must preserve shortest-path correctness.

Do not use the original graph for the backward search in directed graphs.

Do not assume the first meeting vertex is necessarily the geometric midpoint of the path.

Do not forget that path reconstruction requires two predecessor trees.

Do not expect benefits when the graph diameter is very small.

Practical Applications

Bidirectional search appears in:

  • GPS routing
  • Road-navigation engines
  • Airline route planning
  • Social-network connection search
  • Puzzle solvers
  • State-space search systems
  • Robotics
  • Logistics optimization

Many large-scale routing systems combine bidirectional search with heuristic techniques such as A*.

Takeaway

Bidirectional search reduces shortest-path exploration by searching simultaneously from the source and the destination. Instead of exploring an entire search radius from one side, it explores roughly half the distance from each side and meets in the middle. The resulting reduction in explored vertices can be exponential in practice, making bidirectional search one of the most effective optimizations available for source-to-target shortest-path problems.