Algorithm Search⌘K

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
bidirectional_dijkstra.pyRawGitHub
# 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

Read the full page about Bidirectional Dijkstra