[Python algorithm] Bidirectional Dijkstra algorithm Python implementation

Python implementation of bidirectional Dijkstra algorithm

Article directory

  • Bidirectional Dijkstra algorithm Python implementation
    • Introduction
    • Advantages of Bidirectional Dijkstra Algorithm
    • limitation
    • Basic steps of the algorithm
      • Termination condition
    • The basic steps
    • pseudocode
    • Python implementation
    • Comparison of two-way Dijkstra and one-way Dijkstra algorithms

Introduction

Bidirectional Dijkstra Algorithm is an algorithm used to find the shortest path between two vertices in a weighted graph. It is a variant of Dijkstra’s algorithm. The basic idea is: search from two directions simultaneously Start searching – perform Dijkstra’s algorithm search simultaneously from the starting point to the end point and from the end point to the starting point. If there is a path, then the search in the two directions will eventually meet and terminate at a certain point, and this path is the shortest distance path. In some cases, the bidirectional Dijkstra’s algorithm can reduce the search space size, thereby increasing the algorithm’s efficiency. There is also the idea of divide and conquer

Advantages of the two-way Dijkstra algorithm

Compared with Dijkstra’s algorithm, the bidirectional Dijkstra algorithm has more advantages in the following situations:

  1. Large-scale graph search: If the size of the graph is large, the bidirectional Dijkstra algorithm is likely to be faster than Dijkstra’s algorithm.

  2. Sparse graphs: In sparse graphs, the bidirectional Dijkstra algorithm is likely to be faster than Dijkstra’s algorithm.

The main reason is that bidirectional search can reduce a certain amount of search space. Searching from the end point and starting from the starting point is equivalent to pruning.

image-20231113185857959

(I think this picture vividly explains why the bidirectional Dijkstra algorithm can reduce the search space, while the single-term Dijkstra algorithm will become more and more divergent in large-scale graphs)

Limitations

  1. Complexity: The bidirectional Dijkstra algorithm requires more complex data structures to keep track of search paths in both directions. The algorithm requires more time to maintain queues in both directions.
  2. Graphs with unequal weights and negative weights: For graphs with large differences in edge weights and negative weights, the two-way Dijkstra algorithm may not perform well.
  3. When the distance between the end point and the starting point is relatively close, the bidirectional Dijkstra algorithm may not be as good as the single Dijkstra algorithm.

But bidirectional Dijkstra still has the advantages of reducing the search space and searching the shortest path faster.

Basic steps of the algorithm

Termination condition

The most important thing about the two-way Dijkstra algorithm is the termination condition, when the algorithm should terminate, and how to determine the meeting point at which the algorithm should terminate. The two-way Dijkstra algorithm needs to maintain two queues, one is the queue from the starting point to the end point queue_from_start and queue_from_target, assuming:

t

o

p

f

top_f

topf? is the head element of the queue_from_start priority queue,

t

o

p

r

top_r

topr? is the head element of the queue_from_target priority queue,

μ

\mu

μ is used to record the path value formed by the meeting point, initialized

μ

=

\mu = \infty

μ=∞. When performing path search, when there is an edge

(

u

,

v

)

(u,v)

(u,v) satisfies

u

u

u is in forward search, while

v

v

v In reverse search, if

d

f

(

u

)

+

c

(

u

,

v

)

+

d

r

(

v

)

< μ d_f(u) + c(u,v) + d_r(v) < \mu df?(u) + c(u,v) + dr?(v)<μ, then update μ \mu μ value.

Termination condition:

t

o

p

f

+

t

o

p

r

μ

top_f + top_r \ge \mu

topf? + topr?≥μ

Bidirectional Dijkstra algorithm

Basic steps

  1. Initialization: Add the starting point to the pending queue for forward search, and add the end point to the pending queue for reverse search.
  2. Iterative search: In each iteration, the algorithm processes the vertices in the forward and reverse queues to be processed respectively, and selects the vertex with the smallest current distance for expansion.
  3. Extend vertices: For the selected vertex, the algorithm updates the shortest path estimates of its adjacent vertices and adds these adjacent vertices to the corresponding set to be processed.
  4. Check for meeting: After each expansion, the algorithm checks whether the forward and reverse searches meet at a vertex. The encounter condition is usually to check whether a vertex appears in both the forward and reverse access lists.
  5. Path reconstruction: Once the search encounters, the algorithm uses forward and reverse path information to reconstruct the shortest path from the starting point to the end point.
  6. Termination condition: If the two searches meet somewhere in the middle, or the search in one direction cannot find new vertices for expansion, the algorithm terminates.

Pseudocode

Program structure:

function BidirectionalDijkstra(graph, start, end)
    create priority queues queueFromStart, queueFromEnd
    add start to queueFromStart with priority 0
    add end to queueFromEnd with priority 0
    create distance maps distanceFromStart, distanceFromEnd and set all distances to infinity
    set distanceFromStart[start] to 0
    set distanceFromEnd[end] to 0
    create parent maps parentFromStart, parentFromEnd and set all parents to null
    set parentFromStart[start] to start
    set parentFromEnd[end] to end

    while queueFromStart and queueFromEnd are not empty
        nodeFromStart = extract minimum from queueFromStart
        for each neighbor of nodeFromStart
            if distance through nodeFromStart to neighbor is less than distanceFromStart[neighbor]
                update distanceFromStart[neighbor]
                update parentFromStart[neighbor]
                add neighbor to queueFromStart with priority distanceFromStart[neighbor]

        nodeFromEnd = extract minimum from queueFromEnd
        for each neighbor of nodeFromEnd
            if distance through nodeFromEnd to neighbor is less than distanceFromEnd[neighbor]
                update distanceFromEnd[neighbor]
                update parentFromEnd[neighbor]
                add neighbor to queueFromEnd with priority distanceFromEnd[neighbor]

        if any node v is in both queueFromStart and queueFromEnd
            path = shortest path from start to v according to parentFromStart
            path = path + reverse of shortest path from v to end according to parentFromEnd
            return path

    return no path
\t


Python implementation

def bidirectional_dijkstra_b(graph, start, target, i):

    queue_from_start = []
    hq.heappush(queue_from_start, (0.0, start))
    distance_from_start = {<!-- -->node: float('infinity') for node in graph}
    distance_from_start[start] = 0.0
    parents_of_start = {<!-- -->start: None}

    queue_from_target = []
    hq.heappush(queue_from_target, (0.0, target))
    distance_from_target = {<!-- -->node: float('infinity') for node in graph}
    distance_from_target[target] = 0.0
    parents_of_target = {<!-- -->target: None}

    close_of_start = set() # Access the closed table
    close_of_target = set() # Access the closed table

    miu = math.inf
    global node
    node=None

    while queue_from_start and queue_from_target:

        if queue_from_start[0][0] + queue_from_target[0][0] >= miu:
            return reverse_traversal(node, parents_of_start, parents_of_target)

        cur_dist, cur_node = hq.heappop(queue_from_start)
        close_of_start.add(cur_node)
        for adjacent, weight in graph[cur_node].items():
            
            if adjacent in close_of_start:
                continue
            distance = cur_dist + weight["weight"]

            if distance < distance_from_start[adjacent]:
                distance_from_start[adjacent] = distance
                parents_of_start[adjacent] = cur_node
                hq.heappush(queue_from_start, (distance, adjacent))
                # Update miu value
                if adjacent in close_of_target:
                    dist = distance + distance_from_target[adjacent]
                    if miu > dist:
                        miu = dist
                        node=adjacent
                        
        cur_dist, cur_node = hq.heappop(queue_from_target)

        close_of_target.add(cur_node)
        for adjacent, weight in graph[cur_node].items():
            if adjacent in close_of_target:
                continue
            distance = cur_dist + weight["weight"]
            if distance < distance_from_target[adjacent]:
                distance_from_target[adjacent] = distance
                parents_of_target[adjacent] = cur_node
                hq.heappush(queue_from_target, (distance, adjacent))

                if adjacent in close_of_start:
                    dist = distance + distance_from_start[adjacent]
                    if miu > dist:
                        miu = dist
                        node=adjacent
                       
    return []

Comparison of two-way Dijkstra and one-way Dijkstra algorithms

Two versions of the bidirectional Dijkstra algorithm have been written, mainly due to different control methods.

The standard for generating sparse graphs in experiments is,

e

=

n

?

log

?

n

e=n*\log{n}

e=n?logn

test_result1
test_result

It can be seen that as the number of nodes and edges increases, the algorithm becomes more time-consuming. When the number of nodes = 50000 and the number of edges = 252959, the speed of Dijkstra’s algorithm is much higher than that of the two-way Dijkstra algorithm. algorithm.

From the above figure, when the number of nodes is on the order of 50,500, the second control flow Dijkstra algorithm is faster, but as the number of nodes increases, the first control flow is bidirectional. The pull algorithm is better. Speculative conclusion: The two-way Dijkstra algorithm contains the idea of divide and conquer. The more equal the number of explorations on both sides, the better.