Radix Heaps ----------- In general, Dijkstra's algorithm works on directed graphs with non-negative real-valued edge costs. Under this criteria, the most efficient implementations of Dijkstra's algorithm, using a Fibonacci heap or similar priority queue, achieves a time complexity of O(m + n log n) where n is the number of vertices in the graph and m is the number of edges. This is the best-achievable time complexity for any implementation of Dijkstra's algorithm based on the comparison and addition of real-valued edge costs. However, if edge costs are limited to moderately sized integers, then the efficiency of Dijkstra's algorithm can be improved by using integer-based priority queues. The radix heap implementation of Dijkstra's algorithm provides better efficiency for many shortest path problems that can be accurately represented using only moderately sized integer edge costs. The radix heap, invented by Ahuja, Melhorn, Orlin, and Tarjan, assumes that all edge costs in the graph are integers bounded above by C. The result is that Dijkstra's algorithm can be implemented with a time complexity of O(m + n log C). Furthermore, the radix heap is significantly simpler to implement compared to the Fibonacci heap and similar data structures. The radix heap achieves this time complexity by taking advantage of the property that shortest path distances fall into a finite range during the computation shortest paths by Dijkstra's algorithm. Recall that Dijkstra's algorithm computes shortest path distances d[v] from some source vertex s to all other vertices v. Initially d[s] = 0. As vertices are explored, they are assigned a tentative value for d[v], which is eventually reduced to the final shortest path distance. Each explored vertex v is inserted into the frontier set F, using an insert() operation, and held there until the value of d[v] becomes minimum among vertices in F. For the minimum vertex u among those in F, it is known that the value of d[u] cannot improved further and is therefore the final shortest path distance. This minimum vertex is removed from F using a delete-min() operation, and then placed permanently in the solution set S. Further vertices v are then explored through outgoing edges of u. Any explored vertex v that is not in S will be placed in F, using an insert() operation, and be assigned a tentative distance value d[v], unless it is already being held there. If an explored vertex is already held in F, then its tentative distance value d[v] may be improved by a decrease-key() operation. Dijkstra's algorithm repeats this delete-min() process until all vertices have been moved to S, with their corresponding shortest path distance assigned in d[v]. The radix heap relies on the following properties of Dijkstra's algorithm: 1. The value of d[v] is limited to the range [0..nC]. 2. The value of d[v] for any vertex v in F lies in the range [d[u]..d[u]+C], where u is the minimum vertex most recently removed from F. Property 1 is seen by noting that no path can contain more than n edges, each of which has cost no larger than C. Property 2 arises because any value d[v], for v in F, extends from some d[w] for w in S. Thus, with d[w]<=d[u] and edge cost at most C, the upper bound on any d[v] is d[u]+C, and the lower bound is d[u] (the minimum distance in F). Let d[u] be denoted as dmin for this description. At any time during Dijkstra's algorithm, the range [dmin..dmin+C] consists of C+1 distinct values. The radix heap divides the C+1 values in this range into B buckets, where B = log2(C+1) + 2 (rounded up to the nearest integer). Buckets are numbered from 1 to B. Each bucket i has an associated size size(i), which is the number of distinct values it covers. The first bucket covers only the minimum value dmin. The second bucket covers the next value, and remaining buckets cover twice the number of values as the bucket before them, except for the last bucket which is allowed to cover nC+1 distinct values (a practically unlimited number). This defines size(i) as follows: size(1) = 1 size(2) = 1 size(i) = 2*size(i-1) (3 <= i <= B-1) size(B) = nC+1 Equivalently: size(1) = 1 size(i) = 2^(i-2) (2 <= i <= B-1) size(B) = nC+1 Conceptually, the interval [dmin..dmin+C] is partitioned, with each bucket i covering a range of values, denoted as range(i), according to size(i). For example: i size(i) range(i) 1 1 dmin 2 1 dmin+1 3 2 dmin+2,...,dmin+3 4 4 dmin+4,...,dmin+7 5 8 dmin+8,...,dmin+15 6 16 dmin+16,...,dmin+31 7 32 dmin+32,...,dmin+63 etc... The range of values covered by a bucket is managed by specifying an upper bound u[i] for each bucket i. In general, bucket i can hold values in the range [u[i-1]+1,...,u[i]]; assuming the convention u[0]=dmin-1. The vertices in F are stored in these buckets according to their distance value d[v], with vertex v in bucket i if d[v] lies in range(i). As the computation proceeds, the upper bounds u[i] are updated, altering the range of buckets. We see later that the value of upper bounds u[i] only increases as the computation proceeds. The inequality |range(i)|<=size(i) is always maintained. The buckets in the radix heap are implemented using doubly-linked lists. This is to allow efficient (constant time) removal of vertices from lists. Initially, when dmin = 0, the upper bounds of buckets are assigned as u[i] = 2^(i-1)-1 for 1<=i<=B-1 and u(B)=nC+1. Dijkstra's algorithm uses the radix heap through the following three operations: - delete-min() for deleting and returns the minimum vertex from F. - insert(v,k) for inserting a vertex v into the F with a key k corresponding to the value of d[v]. - decrease-key(v,k) for decreasing the key of a vertex in F to a new value k corresponding to value of d[v]. The insert(v,k) operation is implemented as follows: First, visit buckets i in decreasing order, starting with i = B, until locating the largest i for which u[i]bucket), the placeNode() method then places the node back in the appropriate bucket of the heap, according to the nodes new key value; refer to the general description of decrease-key(). As described deleteMin() is the most complicated operation. If bucket 1 is empty (that is, if the header of bucket 1 points to itself), then any node can be removed from bucket 1 and its item returned. In this case, the node obtained by header->next is removed from bucket 1 using the removeNode() method. Then minItem is assigned the nodes item (vertex number) before deleting the node and erasing its entry in the node lookup array. Finally, the number of items in the heap is decreased by one and minItem returned. In cases where bucket 1 is empty, the first non-empty bucket (bucket i) is searched for (by locating a bucket header that does not point to itself). The circular linked-list representing the non-empty bucket is then scanned to determine the minimum node in the bucket. This is done by updating the minNode pointer and minKey value whenever a node with a key smaller than minKey is found. With the minimum node located, the upper bound u[j] of lower indexed buckets j < i is updated according to the value of minKey. Starting from bucket i-1, the placedNode() function is then used to reinsert the nodes of bucket i into lower indexed buckets, according to their new upper bounds. The pointers of the header node of bucket i are then reset to reflect that bucket i is now empty. Then minItem is assigned the nodes item (vertex number) before, deleting the node and erasing its entry in the node lookup array. Finally, the number of items in the heap is decreased by one and minItem returned. As described, the placeNode() method is used when manipulating the heap to place a node into its appropriate bucket. The parameter node is a pointer to the node, and the parameter startBucket specifies that the node belongs to a bucket i <= startBucket. The variable, key, is assigned the nodes key. By decreasing i, the first bucket i <= startBucket, that violates u[i] >= key is located. Finally. the node is inserted into bucket i+1 by calling insertNode(). Given a pointer to a node, and a bucket index i, the insertNode() method inserts the node into the circular doubly-linked list corresponding bucket i. First, the node's bucket number is updated. Then the node is inserted between the last actual node in the list (prevNode) and the header node (tailNode) by updating the pointers node->next, tailNode->prev, node->prev, and prevNode->next. The removeNode() function is used to remove a node from its corresponding bucket when manipulating the heap. The single parameter, node, is a pointer to the node. The node is removed simply by updating the next and prev pointers of neighbouring nodes (node->prev and node->next) in the linked list.