Dijkstra
Graph Notation
- s - The start vertex
- d[v] - The current shortest path distance from s to v
- $\Pi$[v] - The predecessor of v on the shortest path from s to v
- $\delta$[u, v] - The actual shortest path length from u to v
- w(u, v) - The weight function w gives us the weight of the path going from u to v, given that (u,v) $\in$ E, the set of edges
Relaxation of Edges
We “relax” an edge if we find a shorter path to the vertex the edge is pointing to than we currently know.
We look at an edge (u, v). If we find that the path from u to v is shorter than d[v], we update the value d[v] to be this new shorter path. We keep doing this for all edges and eventually, we end up with the shortest path to every vertex v starting from the start vertex s.
Relax(u, v, w):
if d[v] > d[u] + w(u, v):
d[v] = d[u] + w(u, v)
predecessor[v] = u
The algorithm we talked about in the last lecture just picked edges to relax randomly, but that algorithm does very poorly complexity-wise. Keeping the general idea the same, if we are a little smarter about how we pick the edges we want to relax, we get a much better algorithm (Dijkstra or Bellman-Ford).
The General Algorithm
The general algorithm works only for directed acyclic graphs which may or may not have negative edges. The algorithm works in 2 main steps
- Sort the graph topologically using depth first search.
- If we use DFS to sort topologically, we can say that a shortest path exists from u to v iff u comes before v in the ordering that DFS gives us.
- Represent the sorted graph linearly and choose a start vertex.
- Go through the sorted linear graph in order from left to right, relax each edge you encounter as you go, and by the end, you have found all the shortest paths.
- Every vertex to the left of s gets a (s, v) of .
The depth first search takes O(V + E) time, and we only look at each edge once, which takes O(E) time, so our general algorithm takes O(V + E) time.
Dijkstra’s Algorithm
Dijkstra is a greedy algorithm, and it only works for undirected graphs without negative edges. Dijkstra starts with a vertex, relaxes all the edges emanating from it, and assumes that the shortest path to that vertex has been found.
Dijkstra(G, w, s):
solved = set() # S contains the set of solved vertices
Q = priority_queue()
Q.add(V) # Q contains the set of vertices of the graph
d = dict() # d will contain the d values of all the vertices as we go on relaxing them
while Q:
# While there are still elements in Q, do
u = extract_min(Q) # return the element with the lowest priority in Q and remove it from Q.
solved.add(u) # Assume that u has been solved
for v in Adj[u]:
relax(u, v, w)
The priority queue Q will be ordered by the d values in the dictionary d. We implement the priority queue using a min-heap, and calculating the cost of operations required in the implementation above gives us a complexity of $\Theta(V \cdot log\,V+E \cdot log\,V)$. If we implement the priority queue using a fibonacci heap, we get a complexity of $\Theta(V \cdot log\,V+E)$.