a s b c t 2 1 1 2 1 6 4

Given a graph \(G=(E,V)\) and a source node \(s\). Dijkstra finds the shortest weighted path from \(s\) to every other node in the graph. The graph however needs to only have non-negative edge weights. In the above graph the shortest path between nodes \(s\) and \(t\) is \(s \rightarrow a \rightarrow b \rightarrow t\). Dikstra is extremely fast. It can run in amortized time \(O(n\log(n) + m)\) if we implement Dijkstra with a fiponacci heap.

Example

How does Dijkstra work? Using the above example, we maintain a set of not-sure nodes \(\{a, b, c, t, s\}\). We let \(d[v]\) be the current distance from \(s\) to \(v\). Initially we let \(d[v] = \infty\) for any node in not-sure and \(d[s] = 0\) for the source node only.

In the first iteration of Dijkstra, we extract the node with the minimum distance in the not-sure list. This node is \(s\). We then update the distance of each neighbor based on the following,

$$$$ \begin{align*} d[u] &= min(d[u], d[u]+weight(s,u)) \end{align*} $$$$

After updating each neightbor, we mark \(s\) as sure. We should see the following values after this iteration.

Iteration s a b c t
0 (extract \(s\)) 0 2 \(\infty\) 1 \(\infty\)

Iteration 1
We extract the node with the minimum distance again from the not-sure list. This time we extract \(c\). We update all the neighbors and at the end of this iteration, we mark \(c\) as sure.

Iteration s a b c t
0 (extract \(s\)) 0 2 \(\infty\) 1 \(\infty\)
1 (extract \(c\)) 0 2 5 1 7

Final iteration
We continue the same process. We extract the node \(a\) and update its neighbors. We then mark it as sure, meaning that the distance from node \(s\) to node \(a\) is 2 and will not change again. We next extract \(b\) and update the neighbors again. We then mark \(b\) as sure. We finally extract \(t\) which has no out-going edges. At the end of the algorithm, we see that we generated all the shortest paths from \(s\) to all the other nodes in the graph.

Iteration s a b c t
0 (extract \(s\)) 0 2 \(\infty\) 1 \(\infty\)
1 (extract \(c\)) 0 2 5 1 7
2 (extract \(a\)) 0 2 4 1 7
3 (extract \(b\)) 0 2 4 1 5
4 (extract \(t\)) 0 2 4 1 5


Pseudocode

Set all vertices to not-sure
d[v] = infinity for all v in V
d[s] = 0
while there are not-sure nodes {
	pick the not-sure node u with the smallest estimate d[u].
	for each neighbor v of u {
		if (d[v] < d[u] + weight(u,v)) {
			// It is cheaper to reach v from u than the current path
			d[v] = d[u] + weight(u,v)
		}
	}
	Mark u as sure
	// at this point we know that d[u] = distance(s,v) (proof below)
}

Extracting the path

To reconstruct the actuall path take, we just maintain a pointer to the parent node. We simply keep an additional array \(p\) and modify the update step as follows:

p[v] = -1 for all v in V
while there are not-sure nodes {
	if (d[v] < d[u] + weight(u,v)) {
		// we maintain a parent link
		p[v] = u
	}
}

Full Implementation on Github

Proof of Correctness

Why does Dijkstra work? We need to prove two important claims in order to prove that Dijkstra is correct.

Claim 1: For all \(v\), \(d[v] \geq d(s,v)\). That is, \(d[v]\) will never be an underestimate for any node \(v\).

Proof:
Inducive Hypothesis: After \(t\) iterations, \(d[v] \geq d(s,v)\) for all \(v\).

Base Case: After 0 iterations, the algorithms sets \(d[s]= 0 = d(s,s)\) and sets \(d[v]\) to \(\infty\) for all \(v \neq s\) and therefore we have \(d[v] \geq d(s,v)\), as required.

Inductive Step: Assume that after \(t\) iterations, \(d[v] \geq d(s,v)\) for all \(v\). We will prove the inequality holds after \(t+1\) iterations. At iteration \(t+1\), we pick the minimum not-sure node \(u\) and then update all neighbors \(v\) such that: \(d[v] = min(d[v], d[u]+w(u,v))\). To see that \(d[v] \geq d(s,v)\), notice that:
(1) \(d[v] \geq d(s,v)\) by the inductive hypothesis.
(2) \(d[u] + w(u,v) \geq d(s,v)\). This is because we know that \(d(s,v) \leq d(s,u) + d(u,v)\) and we also know that \(d[u] \geq d(s,u)\) by the inductive hypothesis. Therefore, \(d(s,v) \leq d[u] + d(u,v)\).

Conclusion: After the algorithm terminates, we have \(d[v] \geq d(s,v)\) for all \(v\) in \(V\), as required. \(\blacksquare\)

Claim 2: When a vertex \(u\) is marked sure, \(d[u] = d(s,u)\)

Proof:
Inducive Hypothesis: When the t’th vertex \(v\) is marked as sure, \(d[v] = d(s,v)\).

Base Case: When the first vertex \(s\) is marked sure, we know that \(d[s]=0=d(s,s)\), as required.

Inductive Step: Suppose we’re about to mark vertex \(u\) as sure, and assume every vertex already marked as sure has \(d[v]=d(s,v)\). We will show that \(d[u]=d(s,u)\). Consider a shortest path from \(s\) to \(u\).

s z z' u

We want to prove that \(d[u]=d(s,u)\). Suppose toward a contradiction that our claim is not true and that \(u\) has the wrong estimate. Also suppose that node \(z\) is the last node with a correct estimate before node \(u\) and that vertex \(z'\) is the vertex after \(z\) in the shortest path above. We can see that

$$$$ d[z] = d(s,z) \leq d(s,u) \leq d[u] $$$$

This is because we assumed \(z\) has a correct estimate and we also know that \(d(s,z) \leq d(s,u)\) because first, sub-paths of shortest paths are shortest paths (can be proved by contradiction). Second, the distance from \(z\) to \(u\) is non-negative because all edges have non-negative weights and therefore \(d(s,z) \leq d(s,u)\). The last part \(d(s,u) \leq d[u]\) follows from claim 1!

So now, we have \(d[z] \leq d[u]\). There are two cases:
Case 1: If \(d[z] = d[u]\). In this case, since we assumed \(z\) has a correct estimate then \(u\) must have a correct estimate and we’re done!
Case 2: If \(d[z] < d[u]\). In this case, since \(u\) was the smallest not-sure node, then \(z\) must be sure. Otherwise we would have picked \(z\) as the smallest not-sure node. Since \(z\) is sure then we must have updated \(z\)’s neighbors. In particular, we know that \(z'\) comes after \(z\) so,

$$$$ \begin{align*} d[z'] &\leq d[z] + w(z,z') \\ &= d(s,z) + w(z,z') \ \ \text{This is because } z \text{ is a sure node so by IH } d[z] = d(s,z) \\ &= d(s,z') \ \ \text{ Subpaths of shortest paths are shortest paths} \\ &\leq d[z'] \ \ \text{ By claim 1} \end{align*} $$$$

This means that \(z'\) has a correct estimate. This is a contradiction because we assumed that \(z\) is the last node with a good estimate and therefore, \(u\) must have the correct estimate, as required.

Conclusion: After the last node is marked sure, we have \(d[v] = d(s,v)\) for all \(v\) in \(V\), as we wanted to show. \(\blacksquare\)

Running Time

What are we doing in this algorithm? For each vertex in the not-sure list, we
(1) find the minimum vertex.
(2) remove that vertex.
(3) update all neighbors with lower estimates if possible.

Therefore we see that if we have \(n\) vertices and \(m\) edges:

$$$$ \begin{align*} TotalTime &= \sum_{u \in V} \big\{ T(findMin) + \big(\sum_{v \in u.neighbors} T(updateKey)\big) + T(removeMin) \big\} \\ &= n(T(findMin) + T(removeMin)) + m(T(updateKey)) \end{align*} $$$$

Now it is clear that it really depends on how we implement the list that holds the not-sure nodes. Let’s consider different data structures

  • Arrays
    • findMin will run in \(O(n)\)
    • RemoveMin will run in \(O(n)\)
    • UpdateKey will run in \(O(1)\)
      Therefore, the total time will be \(O(n(2n) + m) = O(n^2 + m) = O(n^2)\)
  • Red Black Tree
    • findMin will run in \(O(\log(n))\)
    • RemoveMin will run in \(O(\log(n))\)
    • UpdateKey will run in \(O(\log(n))\)
      Therefore, the total time will be \(O(n\log(n) + m\log(n)) = O((n+m)\log(n))\).
      Notice here, if the graph is dense, meaning that \(m=O(n^2)\), then this is worse than arrays! if it’s sparse, then it’s better.
  • Fibonacci Heaps
    • findMin will run in \(O(1)\), amortized time.
    • RemoveMin will run in \(O(\log(n))\), amortized time.
    • UpdateKey will run in \(O(1)\), amortized time.
      Therefore, the total time will be \(O(n\log(n) + m)\), amortized time.

Handling Negative Weights

TODO!!

References

CS161 Stanford