Bidirectional Dijkstra in Python
Runs Dijkstra forward from s and backward from t, stopping when the searches meet.
- File
bidirectional_dijkstra.py, 73 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/bidirectional-dijkstra/. The authors are listed in prov/BPW9SV.jsonl.
import heapq
import math
INF = math.inf
def bidirectional_dijkstra(
adj: list[list[tuple[int, float]]], s: int, t: int
) -> tuple[float, list[int] | None]:
"""Return (dist, path): the distance from s to t and a shortest path.
The graph has the vertices 0 .. n-1; adj[u] lists the edges (v, w) out of u.
Precondition: every weight w is nonnegative (a ValueError is raised otherwise).
If t is not reachable from s, returns (math.inf, None). The forward search runs
on adj from s, the backward search on the reversed edges from t, each a Dijkstra
search with a lazy-deletion binary heap. At each step the side whose queue has
the smaller minimum key moves. Time O(n + m log m), extra space O(n + m).
"""
n = len(adj)
radj: list[list[tuple[int, float]]] = [[] for _ in range(n)]
for u in range(n):
for v, w in adj[u]:
if w < 0:
raise ValueError("bidirectional Dijkstra needs nonnegative weights")
radj[v].append((u, w))
if s == t:
return 0, [s]
d = [[INF] * n, [INF] * n] # @line L1
parent: list[list[int | None]] = [[None] * n, [None] * n]
done = [[False] * n, [False] * n]
d[0][s] = 0
d[1][t] = 0
q: list[list[tuple[float, int]]] = [[(0, s)], [(0, t)]] # @line L2
mu = INF
meet: tuple[int, int] | None = None # an edge (a, b) with mu = d[0][a] + w + d[1][b]
graph = (adj, radj)
while True: # @line L3
top = [q[0][0][0] if q[0] else INF, q[1][0][0] if q[1] else INF]
if top[0] + top[1] >= mu: # @line L4
break
x = 0 if top[0] <= top[1] else 1 # @line L5
y = 1 - x
_, u = heapq.heappop(q[x]) # @line L6
if done[x][u]:
continue
done[x][u] = True # @line L7
dx, dy = d[x], d[y]
for v, w in graph[x][u]: # @line L8
if dx[u] + w < dx[v]: # @line L9
dx[v] = dx[u] + w
parent[x][v] = u
heapq.heappush(q[x], (dx[v], v))
if v != u and dx[u] + w + dy[v] < mu: # @line L10
mu = dx[u] + w + dy[v]
meet = (u, v) if x == 0 else (v, u)
if meet is None: # @line L11
return INF, None
a, b = meet
front: list[int] = []
x_: int | None = a
while x_ is not None:
front.append(x_)
x_ = parent[0][x_]
front.reverse()
back: list[int] = []
x_ = b
while x_ is not None:
back.append(x_)
x_ = parent[1][x_]
return mu, front + back
test_bidirectional_dijkstra.py 79 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/bidirectional-dijkstra/. The authors are listed in prov/BPW9SV.jsonl.
import math
import random
import pytest
from bidirectional_dijkstra import bidirectional_dijkstra
INF = math.inf
# s=0, a=1, b=2, x=3, t=4
EXAMPLE = [[(1, 3), (3, 6)], [(2, 4)], [(4, 3)], [(4, 6)], []]
def bellman_ford(adj, s):
"""Independent oracle: distances from s by n - 1 rounds of relaxing every edge."""
n = len(adj)
d = [INF] * n
d[s] = 0
for _ in range(n - 1):
for u in range(n):
for v, w in adj[u]:
if d[u] + w < d[v]:
d[v] = d[u] + w
return d
def path_weight(adj, p):
return sum(min(w for v, w in adj[a] if v == b) for a, b in zip(p, p[1:]))
def test_example() -> None:
assert bidirectional_dijkstra(EXAMPLE, 0, 4) == (10, [0, 1, 2, 4])
def test_meeting_vertex_is_not_on_the_shortest_path() -> None:
# x is the first vertex scanned by both searches if they alternate, but s-x-t costs 12
dist, p = bidirectional_dijkstra(EXAMPLE, 0, 4)
assert dist == 10 and 3 not in p
def test_unreachable() -> None:
adj = [[(1, 2)], [], [(0, 1)]]
assert bidirectional_dijkstra(adj, 0, 2) == (INF, None)
assert bidirectional_dijkstra([[], []], 0, 1) == (INF, None)
def test_same_vertex_and_self_loops() -> None:
assert bidirectional_dijkstra([[(0, 0)]], 0, 0) == (0, [0])
adj = [[(0, 0), (1, 5)], [(1, 0)]]
assert bidirectional_dijkstra(adj, 0, 1) == (5, [0, 1])
def test_zero_weights_and_parallel_edges() -> None:
adj = [[(1, 0), (1, 3)], [(2, 0), (0, 0)], [(0, 0)]]
assert bidirectional_dijkstra(adj, 0, 2) == (0, [0, 1, 2])
def test_negative_weight_rejected() -> None:
# with a negative edge (and here a negative cycle 0 -> 1 -> 0) there is no valid answer
with pytest.raises(ValueError):
bidirectional_dijkstra([[(1, 1)], [(0, -2)]], 0, 1)
def test_random_against_bellman_ford() -> None:
rng = random.Random(1969)
for _ in range(800):
n = rng.randint(1, 9)
adj = [[] for _ in range(n)]
for _ in range(rng.randint(0, 3 * n)):
adj[rng.randrange(n)].append((rng.randrange(n), rng.choice([0, rng.randint(1, 9)])))
s, t = rng.randrange(n), rng.randrange(n)
dist, p = bidirectional_dijkstra(adj, s, t)
assert dist == bellman_ford(adj, s)[t]
if dist == INF:
assert p is None
else:
assert p[0] == s and p[-1] == t and path_weight(adj, p) == dist
What the next level needs
- the tests did not run in this build