3

I am implementing a symmetric bidirectional A* shortest path algorithm, as mentioned in [Goldberg and Harrelson,2005]. This is only for understanding the algorithm, therefore I used the most basic version without any optimization steps.

My problem is the bidirectional algorithm appears to scan almost two times the number of edges scanned in a uni-directional A* search on the test graph.

Example: a s-t query on a road network using A* (left) and bidirectional A* (right). Nodes scanned by the forward and backward search are colored in red and green, respectively. The heuristic function is simply the euclidean distance to t or s. The computed paths (blue) are correct in both figures.

A* search example

I may be understanding the algorithm logic incorrectly. Here is how I adapted unidirectional A* to bidirectional (from reference)

  • Alternate between forward search and backward search. Maintain variable mu as the current best estimate of s-t path length.
  • In the forward search, if w in edge (v,w) has been scanned by the backward search, do not update w's labels.
  • Each time a forward search scan (v,w) and if w has been scanned by the reverse search, update mu if dist(s,v) + len(v,w)+dist(w,t)<= mu
  • Stopping condition: when forward search is about to scan a vertex v with dist(s,v) + potential_backward(v) >= mu
  • (Similar rules apply in the backward search)

I'd appreciate if anyone can point out the flaws in my implementation, or some more detailed explanation of the bidirectional A* algorithm.

Code in Python:

""" 
bidirectional_a_star: bidirectional A* search 

g: input graph (networkx object)
s, t: source and destination nodes
pi_forward, pi_backward: forward and backward potential function values
wt_attr: attribute name to be used as edge weight 
"""

def bidirectional_a_star(g,s,t,pi_forward, pi_backward, wt_attr='weight' ):
    # initialization 
    gRev = g.reverse() # reverse graph        
    ds =   { v:float('inf') for v in g } # best distances from s or t
    dt = ds.copy()
    ds[s]=0
    dt[t]=0  
    parents = {} # predecessors in forward/backward search
    parentt = {}
    pqueues =[(ds[s]+pi_forward[s],s)]  # priority queues for forward/backward search
    pqueuet = [(dt[t]+pi_backward[t],t)]

    mu = float('inf') # best s-t distance

    scanned_forward=set() # set of scanned vertices in forward/backward search
    scanned_backward=set()

    while (len(pqueues)>0 and len(pqueuet)>0):
        # forward search
        (priority_s,vs) = heappop(pqueues) # vs: first node in forward queue

        if (priority_s >= mu): # stop condition
            break

        for w in g.neighbors(vs): # scan outgoing edges from vs
            newDist = ds[vs] + g.edge[vs][w][wt_attr]            

            if (ds[w] > newDist and w not in scanned_backward):                
                ds[w] = newDist  # update w's label
                parents[w] = vs
                heappush(pqueues, (ds[w]+pi_forward[w] , w) )

            if ( (w in scanned_backward) and  (newDist + dt[w]<mu)):
                 mu = newDist+dt[w]

        scanned_forward.add(vs)  # mark vs as "scanned" 

        # backward search
        (priority_t,vt) = heappop(pqueuet) # vt: first node in backward queue

        if (priority_t>= mu ):  
            break

        for w in gRev.neighbors(vt): 
            newDist = dt[vt] + gRev.edge[vt][w][wt_attr]

            if (dt[w] >= newDist and w not in scanned_forward):
                if (dt[w] ==newDist and parentt[vt] < w):
                    continue
                else:
                    dt[w] = newDist
                    parentt[w] = vt
                    heappush(pqueuet,(dt[w]+pi_backward[w],w))
            if ( w in scanned_forward and  newDist + ds[w]<= mu):
                 mu = newDist+dt[w]

        scanned_backward.add(vt)

    # compute s-t distance and shortest path
    scanned = scanned_s.intersection(scanned_t)    
    minPathLen = min( [ ds[v]+dt[v] for v in scanned ] ) # find s-t distance   
    minPath = reconstructPath(ds,dt,parents,parentt,scanned) # join s-v and v-t path

    return (minPathLen, minPath)

Update

Following Janne's comment, I created a demo that tests the search on a few examples. The implementation has been improved, and fewer nodes are scanned.

Example: Shortest path from the red dot to the green dot on a (directed) grid graph. The middle figure highlights nodes scanned by A*; The right figure shows nodes scanned by the forward search (orange) and those scanned by the backward search (blue) toy example

However, on the road network, the union of nodes scanned by the forward search and those scanned by the backward search is still more than the number of nodes scanned by a unidirectional search. Perhaps this depends on the input graph?

Community
  • 1
  • 1
yang
  • 117
  • 1
  • 2
  • 7
  • How are you logging/recording visited edges? – sje397 Sep 17 '15 at 06:55
  • The algorithm adds labels on vertices rather than edges. e.g. I used `scanned_forward` and `scanned_backward` to store scanned /settled vertices. I also store the predecessor of every node on the two shortest path trees in `parents` and `parentt`. Distances to source or destination of those nodes are stored in `ds` and `dt` – yang Sep 17 '15 at 07:03
  • 1
    I'm not sure if it's the cause of your issues, but your initialization of `ds` and `dt` (`ds = dt = { v:float('inf') for v in g }`) makes them both references to the same dictionary. – Blckknght Sep 17 '15 at 07:31
  • thanks for spotting that, but in fact I only wrote them in one instruction to save space for posting. In the original code, variables are initialized separately. I will change that in my question – yang Sep 17 '15 at 07:39
  • 1
    Have you tested with a small graph? Could you post a runnable example including small and simple data? – Janne Karila Sep 22 '15 at 09:15
  • Sorry for the late response. I posted a runnable demo – yang Oct 01 '15 at 04:37

1 Answers1

-3

Hy, Your problem is that, you don't put the right condition to stop, the stop condition is when (forward and backward search are meet),