In [ ]:
%%HTML
<style>
.container { width:100% } 
</style>

Dijkstra's Shortest Path Algorithm


In [ ]:
%run Heap-Array.ipynb

In [ ]:
def shortest_path(source, Edges):
    Distance = { source: 0 }
    Visited  = { source }  # this set is only needed for visualization
    Fringe   = []          # priority queue, organized as array based heap
    insert(Fringe, (0, source))
    while Fringe != []:
        display(heapToDot(Fringe))
        d, u = remove(Fringe)
        display(toDot(source, u, Edges, Fringe, Distance, Visited))
        print('_' * 80)
        for v, l in Edges[u]:
            dv = Distance.get(v, None)
            if dv == None or d + l < dv:
                if dv != None:
                    elevate(Fringe, v, d + l)
                else:
                    insert(Fringe, (d + l, v))
                Distance[v] = d + l
        Visited |= { u }
    display(toDot(source, None, Edges, Fringe, Distance, Visited))
    return Distance

Code to Display the Directed Graph


In [ ]:
import graphviz as gv

The function $\texttt{toDot}(\texttt{source}, \texttt{Edges}, \texttt{Fringe}, \texttt{Distance}, \texttt{Visited})$ takes a graph that is represented by its Edges, a set of nodes Fringe, and a dictionary Distance that has the distance of a node from the node source, and set Visited of nodes that have already been visited.


In [ ]:
def toDot(source, p, Edges, Fringe, Distance, Visited):
    V = set()
    for x in Edges.keys():
        V.add(x)
    dot = gv.Digraph(node_attr={'shape': 'record', 'style': 'rounded'})
    dot.attr(rankdir='LR', size='8,5')
    for x in V:
        if x == source:
            dot.node(str(x), color='blue', shape='doublecircle')
        else:
            d = str(Distance.get(x, ''))
            if x == p:
                dot.node(str(x), label='{' + str(x) + '|' + d + '}', color='magenta')
            elif x in Distance and (Distance[x], x) in Fringe:
                dot.node(str(x), label='{' + str(x) + '|' + d + '}', color='red')
            elif x in Visited:
                dot.node(str(x), label='{' + str(x) + '|' + d + '}', color='blue')
            else:
                dot.node(str(x), label='{' + str(x) + '|' + d + '}')
    for u in V:
        for v, l in Edges[u]:
            dot.edge(str(u), str(v), label=str(l))
    return dot

Code for Testing


In [ ]:
a = Node('a')
b = Node('b')
c = Node('c')
d = Node('d')
e = Node('e')
f = Node('f')
g = Node('g')
h = Node('h')

In [ ]:
Edges = { a: [ (c, 2), (b, 9)], 
          b: [(d, 1)],
          c: [(e, 5), (g, 3)],  
          d: [(f, 2), (e, 4)],  
          e: [(f, 1), (b, 2)],
          f: [(h, 5)],
          g: [(e, 1)],
          h: []
        }

In [ ]:
sp = shortest_path(a, Edges)
sp

Crossing the Tunnel

Four persons, Alice, Britney, Charly and Daniel have to cross a tunnel. The tunnel is so narrow, that at most two persons can cross it together. In order to cross the tunnel, a torch is needed. Together, they only have a single torch.

  1. Alice is the fastest and can cross the tunnel in 1 minute.
  2. Britney needs 2 minutes to cross the tunnel.
  3. Charly is slower and needs 4 minutes.
  4. Daniel is slowest and takes 5 minutes to cross the tunnel.

What is the fastest plan to cross the tunnel?


In [ ]:
All  = frozenset({ 'Alice', 'Britney', 'Charly', 'Daniel', 'Torch' })
Time = { 'Alice': 1, 'Britney': 2, 'Charly': 4, 'Daniel': 5, 'Torch': 0 }

The function $\texttt{power}(M)$ defined below computes the power list of the set $M$, i.e. we have: $$ \texttt{power}(M) = 2^M = \bigl\{A \mid A \subseteq M \bigr\} $$


In [ ]:
def power(M):
    if M == set():
        return { frozenset() }
    else:
        C  = set(M)  # C is a copy of M as we don't want to change the set M
        x  = C.pop() # pop removes the element x from the set C
        P1 = power(C)
        P2 = { A | {x} for A in P1 }
        return P1 | P2

If $B$ is a set of persons, then $\texttt{duration}(B)$ is the time that this group needs to cross the tunnel. $B$ also contains 'Torch'.


In [ ]:
NodeDict = { S: Node(S) for S in power(All) }

In [ ]:
def duration(B):
    return max(Time[x] for x in B)

$\texttt{left_right}(S)$ describes a crossing of the tunnel from left to right.


In [ ]:
def left_right(S):
    return [(NodeDict[S - B], duration(B)) for B in power(S) if 'Torch' in B and 2 <= len(B) <= 3]

$\texttt{right_left}(S)$ describes a crossing of the tunnel from right to left.


In [ ]:
def right_left(S):
    return [(NodeDict[S | B], duration(B)) for B in power(All - S) if 'Torch' in B and 2 <= len(B) <= 3]

In [ ]:
Edges = { Node(S): left_right(S) + right_left(S) for S in power(All) }
len(Edges)

The function shortest_path is Dikstra's algorithm. It returns both a dictionary containing the parents and a dictionary with the distances.


In [ ]:
def shortest_path(source, Edges):
    Distance = { source: 0 }
    Parent   = {}
    Fringe   = []
    insert(Fringe, (0, source) )
    while Fringe != []:
        d, u = remove(Fringe)
        for v, l in Edges[u]:
            dv = Distance.get(v, None)
            if dv == None or d + l < dv:
                if dv != None:
                    elevate(Fringe, v, d + l)
                else:
                    insert(Fringe, (d + l, v))
                Distance[v] = d + l
                Parent[v] = u
    return Parent, Distance

In [ ]:
start = Node(frozenset(All))

In [ ]:
Parent, Distance = shortest_path(start, Edges)

Let us see whether the goal was reachable and how long it takes to reach the goal.


In [ ]:
goal = NodeDict[frozenset()]
Distance[goal]

Given to nodes source and goal and a dictionary containing the parent of every node, the function find_path returns the path from source to goal.


In [ ]:
def find_path(source, goal, Parent):
    p = Parent.get(goal)
    if p == None:
        return [source]
    return find_path(source, p, Parent) + [goal]

In [ ]:
Path = [x.mValue for x in find_path(NodeDict[frozenset(All)], NodeDict[frozenset()], Parent)]

In [ ]:
def print_path():
    total = 0
    print("_" * 81);
    for i in range(len(Path)):
        Left  = set(Path[i])
        Right = set(All) - set(Left)
        if Left == set() or Right == set():
            print(Left, " " * 25, Right)
        else:
            print(Left, " " * 30, Right)
        print("_" * 81);
        if i < len(Path) - 1:
            if "Torch" in Path[i]:
                Diff = set(Path[i]) - set(Path[i+1])
                time = duration(Diff)
                total += time
                print(" " * 20, ">>> ", Diff, ':', time, " >>>")
            else:
                Diff = set(Path[i+1]) - set(Path[i])
                time = duration(Diff)
                total += time
                print(" " * 20, "<<< ", Diff, ':', time, " <<<")
            print("_" * 81)
    print('Total time:', total, 'minutes.')

In [ ]:
print_path()

In [ ]: