Which datatype to use as queue in Dijkstra's algorithm?
Asked Answered
S

5

26

I'm trying to implement Dijkstra's algorithm in Java (self-study). I use the pseudo-code provided by Wikipedia (link). Now near the end of the algorithm, I should decrease-key v in Q;. I guess i should have implemented Q with a BinaryHeap or something like that? What would be the right (built-in) datatype to use here?

private void dijkstra(int source) {
        int[] dist = new int[this.adjacencyMatrix.length];
        int[] previous = new int[this.adjacencyMatrix.length];
        Queue<Integer> q = new LinkedList<Integer>();

        for (int i = 0; i < this.adjacencyMatrix.length; i++) {
            dist[i] = this.INFINITY;
            previous[i] = this.UNDEFINED;
            q.add(i);
        }

        dist[source] = 0;

        while(!q.isEmpty()) {
            // get node with smallest dist;
            int u = 0;
            for(int i = 0; i < this.adjacencyMatrix.length; i++) {
                if(dist[i] < dist[u])
                    u = i;
            }

            // break if dist == INFINITY
            if(dist[u] == this.INFINITY) break;

            // remove u from q
            q.remove(u);

            for(int i = 0; i < this.adjacencyMatrix.length; i++) {
                if(this.adjacencyMatrix[u][i] == 1) {
                    // in a unweighted graph, this.adjacencyMatrix[u][i] always == 1;
                    int alt = dist[u] + this.adjacencyMatrix[u][i]; 
                    if(alt < dist[i]) {
                        dist[i] = alt;
                        previous[i] = u;

                        // here's where I should "decrease the key"
                    }
                }
            }
        }
    }
Soothe answered 7/6, 2011 at 14:56 Comment(1)
You really should use q to get the node with smallest dist, or there is no point in doing decrease-key v in Q; anyway.Dynamism
O
40

The simplest way is to use a priority queue and not to care about the previously added key in the priority queue. This means you will have each node multiple times in the queue, but this does not hurt the algorithm at all. If you have a look at it, all versions of the node which have been replaced will be picked up later and by then the closest distance will already have been determined.

The check if alt < dist[v]: from the wikipedia is what makes this work. The runtime will only degrade a little from this, but if you need the very fast version you have to optimize further.

NOTE:

Like any optimization, this one should be handled with care and may lead to curious and hard to find bugs (see e.g. here). For most cases, just using remove and re-insert should be fine, but the trick I mentioned here, can speed up your code a little if your Dijkstra implementation is the bottleneck.

Most importantly: Before trying this, make sure how your priority queue handles the priority. The actual priority in the queue should never change, or you may mess up invariants of the queue, which means items stored in the queue may not be retrievable any more. E.g. in Java, priorities are stored together with the object, so you do need an additional wrapper:

This will not work:

import java.util.PriorityQueue;

// Store node information and priority together
class Node implements Comparable<Node> {
  public int prio;
  public Node(int prio) { this.prio = prio; }

  public int compareTo(Node n) {
     return Integer.compare(this.prio, n.prio);
  }
}

...
...
PriorityQueue<Node> q = new PriorityQueue<Node>();
n = new Node(10);
q.add(n)
...
// let's update the priority
n.prio = 0;
// re-add
q.add(n);
// q may be broken now

Because at n.prio=0 you are also changing the priority of the object within the queue. However, this will work fine:

import java.util.PriorityQueue;

// Only node information
class Node {
  // Whatever you need for your graph
  public Node() {}
}

class PrioNode {
   public Node n;
   public int prio;
   public PrioNode(Node n, int prio) {
     this.n = n;
     this.prio = prio;
   }

   public int compareTo(PrioNode p) {
      return Integer.compare(this.prio, p.prio);
   }
}

...
...
PriorityQueue<PrioNode> q = new PriorityQueue<PrioNode>();
n = new Node();
q.add(new PrioNode(n,10));
...
// let's update the priority and re-add
q.add(new PrioNode(n,0));
// Everything is fine, because we have not changed the value
// in the queue.
Oshiro answered 7/6, 2011 at 15:28 Comment(13)
Excellent observation. This saved me from rolling my own priority queue.Vibraculum
how do you check for empty queue then? that will hurt the run time considerably for some inputsRubstone
@GiovanniBotta: The same as usual. Of course you have to remove the duplicates from the queue, when you encounter them. Once all are removed the algorithm is done. The run time lies somewhere between the naive implementation using a linked-list for the queue O(|V|^2) and the one using a fibonacci-heap O(|E|+|V|log|V|), as the number of total nodes appearing in the heap at any time, including duplicates, is bounded by |V|. For any node, which is a duplicate, encountering this nodes simply means it can be discarded, which can be done in O(1).Oshiro
@Oshiro so when you pull out an edge from the PQ you need to make sure its distance from the source is correct and discard it if not? That makes sense. I'd like to do a more thorough running time analysis to determine an upper bound but it sounds reasonable.Rubstone
@GiovanniBotta: A more thourough analysis should not be too hard. Basically one just could look at the analysis for Dijkstra's Algorithm with linked-lists or Fibonnacci heaps and modify the access time, adding the repeated O(1) complexity for duplicates. It's been a while since I last looked at those analyses, so I am not sure how they worked.Oshiro
I changed the accepted answer to this, since, by far, most people found this useful.Craver
I do not believe this is sound. If you change an existing node's priority and then insert it again into the same PriorityQueue, it will now exist twice in the underlying heap structure, but one of those positions will be incorrect given its current priority. This violates the heap invariant and could cause future remove operations to fail to remove the lowest value. Specifically, the node whose priority is too low for its current position could block a node from rising as high as it should in an up-heap operation.Cimex
@AdamDingle Ok, it's been quite a while, since I last thought this through. 1. For Dijkstras algorithm, you are always interested in the node with the lowest value. So all that is needed, is a simple "pop". So you always remove the top element. Re-Heapifying is then done by only looking at the priorities, not the actual nodes. Thus, everything will stay heapified after pop. 2. You should never update the prio in the heap, so that the lower elements do not get any incorrect priorities. E.g. you store pairs (priority,value). The lower element will remain, but will have a different prio.Oshiro
@Oshiro But Java's PriorityQueue doesn't work like that. It doesn't keep a separate copy of each node's priority; it simply calls a comparator to ask which nodes precede others in the ordering. I've written more on a web page that includes Java code demonstrating that this technique is unsound.Cimex
@AdamDingle I agree, if you implement it as you did on your page, it certainly will not work. As I said above "You should never update the prio in the heap", and your page demonstrates this to be true. The way to make it work is a clean separation of concerns. 1. Why should nodes have a priority? A general graph structure does not need it, so why store it in the node? 2. Instead of storing the prio in the node, wrap the node with the prio when inserting it in a new object. Now you can insert the same node as often as you like with each inserted node having a different priority.Oshiro
@AdamDingle I agree however, that this technique requires some care, and at least some knowledge of how the priority queue works. This should be handled with the general rules for optimizations put forth by Michael A. Jackson "1. Don't do it 2. (Experts only) Don't do it yet." I'll add a warning to my answer above.Oshiro
@LiKao: Thanks for the detailed update. OK, so now I understand what you meant when you wrote "E.g. you store pairs (priority, value)". I think I now agree that this technique as you've presented it is sound and potentially a nice optimization. Thanks!Cimex
why is there no decrease key implentation in Java priorityqueue ?Xanthin
P
24

You can use a TreeSet, (in C++ you can use a std::set) to implement your priority queue for Dijkstra. A TreeSet represents a set, but we also allowed to describe order of the items in the set. You need to store the nodes in the set and use the distances of the nodes to order the nodes. The node with smallest distance will be at the beginning of the set.

class Node {
    public int id;   // store unique id to distinguish elements in the set
    public int dist; // store distance estimates in the Node itself
    public int compareTo(Node other) {
        // TreeSet implements the Comparable interface.
        // We need tell Java how to compare two Nodes in the TreeSet.
        // For Dijstra, we want the node with the _smallest_ distance
        // to be at the front of the set.
        // If two nodes have same distance, choose node with smaller id
        if (this.dist == other.dist) {
            return Integer.compare(this.id, other.id);
        } else {
            return Integer.compare(this.dist, other.dist);
        }
    }
}
// ...
TreeSet<Node> nodes = new TreeSet<Node>();

The extract-min operation is implemented via the following and takes O(lgn) worst case time:

Node u = nodes.pollFirst();

With the decrease-key operation, we remove the node with the old key (the old distance estimate) and add a new node with the smaller key (the new, better distance estimate). Both operations take O(lgn) worst case time.

nodes.remove(v);
v.dist = /* some smaller key */
nodes.add(v);

Some extra notes:

  • The above is very simple to implement and, because both of these operations are logarithmic in n, overall, the running time will be O((n + e)lgn). This is considered efficient for a basic implemtation of Dijkstra. See the CLRS book (ISBN: 978-0-262-03384-8) Chapter 19 for a proof of this complexity.

  • Although most textbooks will use a priority queue for Dijkstra, Prim, A*, etc., unfortunately neither Java nor C++ actually has a implementation of priority queue with the same O(lgn) decrease key operation!

  • PriorityQueue does exist in Java, but the remove(Object o) method is not logarithmic and so your decrease-key operation would be O(n) instead of O(lgn) and (asymptotically) you'd get a slower Dikjstra!

  • To build up the TreeSet from nothing (using a for loop), it takes time O(nlgn) which is a bit slower compared to the O(n) worst case time to initialise a heap / priority queue from n items. However the main loop of Dijkstra takes time O(nlgn + elgn) which dominates this initialisation time. So for Dijkstra, initialising a TreeSet won't cause any significant slowdown.

  • We can't use a HashSet because we care about the order of the keys - we want to be able to pull out the smallest first. This gives us the node with the best distance estimate!

  • TreeSet in Java is implemented using a Red-Black tree - a self-balancing binary search tree. That's why these operations have logarithmic worst case time.

  • You're using ints to represent you graph nodes which is good but when you introduce a Node class you'll need a way to relate the two entities. I'd recommending building a HashMap<Integer, Node> when you build you graph - it'll help keep track of which int corresponds to what Node. `

Portcullis answered 24/2, 2017 at 16:10 Comment(3)
This doesn't work unless every path has a unique distance. However, I believe this can be fixed by giving each Node a unique id, and changing compareTo() to factor in this id when two Nodes have the same distance.Anatolic
@CraigP.Motlin you're right, a set can not have duplicates. I'll correct my answer to use node ids instance of distances.Portcullis
In the code above, instead of Integer.valueOf(this.id).compareTo(other.id) you could just write Integer.compare(this.id, other.id).Cimex
M
4

The suggested PriorityQueue does not provide a decrease-key operation. However, it can be emulated by removing the element and then reinserting it with the new key. This should not increase the asymptotic run time of the algorithm, although it could be made slightly faster with built-in support.

EDIT: This does increase the asymptotic run time, as decrease-key is expected to be O(log n) for a heap but remove(Object) is O(n). It appears there isn't any built-in priority queue in Java with support for an efficient decrease-key.

Melodeemelodeon answered 7/6, 2011 at 15:8 Comment(3)
Thank you for this one. I think the Apache Commons contain one.. however, I'm going to implement one by myself.. :-)Craver
We can remove (O(log n) top element from the priority queue and reinsert it (O(log n)). In this way all the elements gets reorganized again. <code> Object e = pq.poll(); pq.add(e);</code>Readiness
The issue is when it's not the top element whose priority is changing, but rather something somewhere in the middle.Adaminah
M
1

Priority Queue as per the wiki article. Which suggests that the classic implementation now is to use a "min-priority queue implemented by a Fibonacci heap."

Milord answered 7/6, 2011 at 14:59 Comment(6)
Is using Fibonacci heap always better?Redingote
Green Code: In learning about Dijkstra's it is probably over kill, unless you also wish to learn about implementation of a Fibonacci heap. So in light of learning it's probably not better, in terms of actual performance it would appear to be.Milord
I will give this one a try. Implementing a Fibonacci heap should be fun as well :-)Craver
As other answers mentioned, Java's PriorityQueue doesn't include DecreaseKey, which is needed. So the real answer is that there's no right built in type to use in Java.Beabeach
It should be noted, a Fibonacci heap has pretty poor constants, so in practice one should probably pick another type of heap.Destroyer
Agreed, there is no PQ in java that works for Dijkstra.Rubstone
R
1

Yes, Java doesn't provide the decrease key for Min Heap via PriorityQueue, so the operation of removal would be O(N) which can be optimised to logN.

I have implemented Min Heap with decreaseKey(actually both decreaseKey and increaseKey but here only decreaseKey will suffice). Actual Data Structure is Min Heap Map ( HashMap stores the indices of all Nodes and helps in updating current vertex's neighbour's min path value via current vertex )

I implemented the optimised solution with generics, It took me about 3-4 hours of time to code(my first time), The time complexity is O(logV.E)

Hope this will help!

 package algo;

 import java.util.*;

 public class Dijkstra {

/*
 *
 * @author nalin.sharma
 *
 */

/**
 *
 * Heap Map Data Structure
 * Heap stores the Nodes with their weight based on comparison on Node's weight
 * HashMap stores the Node and its index for O(1) look up of node in heap
 *
 *
 *
 *
 * Example -:
 *
 *                                   7
 *                         [2]----------------->[4]
 *                       ^  |                   ^ \
 *                     /   |                   |   \ 1
 *                 2 /    |                   |     v
 *                 /     |                   |       [6]
 *               /      | 1               2 |       ^
 *             /       |                   |      /
 *          [1]       |                   |     /
 *             \     |                   |    / 5
 *            4 \   |                   |   /
 *               v v                   |  /
 *                [3]---------------->[5]
 *                         3
 *
 *        Minimum distance from source 1
 *         v  | d[v] | path
 *         --- ------  ---------
 *         2 |  2  |  1,2
 *         3 |  3  |  1,2,3
 *         4 |  8  |  1,2,3,5,4
 *         5 |  6  |  1,2,3,5
 *         6 |  9  |  1,2,3,4,6
 *
 *
 *
 *     Below is the Implementation -:
 *
 */

static class HeapMap<T> {
    int size, ind = 0;
    NodeWeight<T> arr [];
    Map<T,Integer> map;

    /**
     *
     * @param t is the Node(1,2,3..or A,B,C.. )
     * @return the index of element in min heap
     */
    int index(T t) {
        return map.get(t);
    }

    /**
     *
     * @param index is the Node(1,2,3..or A,B,C.. )
     * @return Node and its Weight
     */
    NodeWeight<T> node(int index) {
        return arr[index];
    }

    /**
     *
     * @param <T> Node of type <T> and its weight
     */
    static class NodeWeight<T> {
        NodeWeight(T v, int w) {
            nodeVal = v;
            weight = w;
        }
        T nodeVal;
        int weight;
        List<T> path = new ArrayList<>();
    }

    public HeapMap(int s) {
        size = s;
        arr = new NodeWeight[size + 1];
        map = new HashMap<>();
    }

    private void updateIndex(T key, int newInd) {
        map.put(key, newInd);
    }

    private void shiftUp(int i) {
        while(i > 1) {
            int par = i / 2;
            NodeWeight<T> currNodeWeight = arr[i];
            NodeWeight<T> parentNodeWeight = arr[par];
            if(parentNodeWeight.weight > currNodeWeight.weight) {
                updateIndex(parentNodeWeight.nodeVal, i);
                updateIndex(currNodeWeight.nodeVal, par);
                swap(par,i);
                i = i/2;
            }
            else {
                break;
            }
        }
    }

    /**
     *
     * @param nodeVal
     * @param newWeight
     * Based on if the value introduced is higher or lower shift down or shift up operations are performed
     *
     */
    public void update(T nodeVal, int newWeight) {
        int i = ind;
        NodeWeight<T> nodeWeight = arr[map.get(nodeVal)];
        int oldWt = nodeWeight.weight;
        nodeWeight.weight = newWeight;
        if(oldWt < newWeight) {
            shiftDown(map.get(nodeVal));
        }
        else if(oldWt > newWeight) {
            shiftUp(map.get(nodeVal));
        }
    }

    /**
     *
     * @param nodeVal
     * @param wt
     *
     * Typical insertion in Min Heap and storing its element's indices in HashMap for fast lookup
     */
    public void insert(T nodeVal, int wt) {
        NodeWeight<T> nodeWt = new NodeWeight<>(nodeVal, wt);
        arr[++ind] = nodeWt;
        updateIndex(nodeVal, ind);
        shiftUp(ind);
    }

    private void swap(int i, int j) {
        NodeWeight<T> tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    private void shiftDown(int i) {
        while(i <= ind) {
            int current = i;
            int lChild = i * 2;
            int rChild = i * 2 + 1;
            if(rChild <= ind) {
                int childInd = (arr[lChild].weight < arr[rChild].weight) ? lChild : rChild;
                if(arr[childInd].weight < arr[i].weight) {
                    updateIndex(arr[childInd].nodeVal, i);
                    updateIndex(arr[i].nodeVal, childInd);
                    swap(childInd, i);
                    i = childInd;
                }
            }
            else if(lChild <= ind && arr[lChild].weight < arr[i].weight) {
                updateIndex(arr[lChild].nodeVal, i);
                updateIndex(arr[i].nodeVal, lChild);
                swap(lChild, i);
                i = lChild;
            }
            if(current == i) {
                break;
            }
        }
    }

    /**
     *
     * @return
     *
     * Typical deletion in Min Heap and removing its element's indices in HashMap
     *
     */
    public NodeWeight<T> remove() {
        if(ind == 0) {
            return null;
        }
        map.remove(arr[1].nodeVal);
        NodeWeight<T> out = arr[1];
        out.path.add(arr[1].nodeVal);
        arr[1] = arr[ind];
        arr[ind--] = null;
        if(ind > 0) {
            updateIndex(arr[1].nodeVal, 1);
            shiftDown(1);
        }
        return out;
    }
}

/**
 *
 *  Graph representation -: It is an Map(T,Node<T>) of Map(T(neighbour), Integer(Edge's weight))
 *
 */
static class Graph<T> {

    void init(T... t) {
        for(T z: t) {
            nodes.put(z, new Node<>(z));
        }
    }

    public Graph(int s, T... t) {
        size = s;
        nodes = new LinkedHashMap<>(size);
        init(t);
    }

    /**
     *
     * Node class
     *
     */
    static class Node<T> {
        Node(T v) {
            val = v;
        }
        T val;
        //Set<Edge> edges = new HashSet<>();
        Map<T, Integer> edges = new HashMap<>();
    }

    /*static class Edge {
        Edge(int to, int w) {
            target = to;
            wt = w;
        }
        int target;
        int wt;
        }
    }*/

    int size;

    Map<T, Node<T>> nodes;

    void addEdge(T from, T to, int wt) {
        nodes.get(from).edges.put(to, wt);
    }
}

/**
 *
 * @param graph
 * @param from
 * @param heapMap
 * @param <T>
 *
 * Performs initialisation of all the nodes from the start vertex
 *
 */
    private static <T> void init(Graph<T> graph, T from, HeapMap<T> heapMap) {
    Graph.Node<T> fromNode = graph.nodes.get(from);
    graph.nodes.forEach((k,v)-> {
            if(from != k) {
                heapMap.insert(k, fromNode.edges.getOrDefault(k, Integer.MAX_VALUE));
            }
        });
    }


static class NodePathMinWeight<T> {
    NodePathMinWeight(T n, List<T> p, int c) {
        node = n;
        path = p;
        minCost= c;
    }
    T node;
    List<T> path;
    int minCost;
}

/**
 *
 * @param graph
 * @param from
 * @param <T>
 * @return
 *
 * Repeat the below process for all the vertices-:
 * Greedy way of picking the current shortest distance and updating its neighbors distance via this vertex
 *
 * Since Each Vertex V has E edges, the time Complexity is
 *
 * O(V.logV.E)
 * 1. selecting vertex with shortest distance from source in logV time -> O(logV) via Heap Map Data structure
 * 2. Visiting all E edges of this vertex and updating the path of its neighbors if found less via this this vertex. -> O(E)
 * 3. Doing operation step 1 and step 2 for all the vertices -> O(V)
 *
 */

    static <T> Map<T,NodePathMinWeight<T>> dijkstra(Graph<T> graph, T from) {
    Map<T,NodePathMinWeight<T>> output = new HashMap<>();
    HeapMap<T> heapMap = new HeapMap<>(graph.nodes.size());
    init(graph, from, heapMap);
    Set<T> isNotVisited = new HashSet<>();
    graph.nodes.forEach((k,v) -> isNotVisited.add(k));
    isNotVisited.remove(from);
    while(!isNotVisited.isEmpty()) {
        HeapMap.NodeWeight<T> currNodeWeight = heapMap.remove();
        output.put(currNodeWeight.nodeVal, new NodePathMinWeight<>(currNodeWeight.nodeVal, currNodeWeight.path, currNodeWeight.weight));
        //mark visited
        isNotVisited.remove(currNodeWeight.nodeVal);
        //neighbors
        Map<T, Integer> neighbors = graph.nodes.get(currNodeWeight.nodeVal).edges;
        neighbors.forEach((k,v) -> {
            int ind = heapMap.index(k);
            HeapMap.NodeWeight<T> neighbor = heapMap.node(ind);
            int neighborDist = neighbor.weight;
            int currentDistance = currNodeWeight.weight;
            if(currentDistance + v < neighborDist) {
                //update
                neighbor.path = new ArrayList<>(currNodeWeight.path);
                heapMap.update(neighbor.nodeVal, currentDistance + v);
            }
        });
    }
    return output;
}

public static void main(String[] args) {
    Graph<Integer> graph = new Graph<>(6,1,2,3,4,5,6);
    graph.addEdge(1,2,2);
    graph.addEdge(1,3,4);
    graph.addEdge(2,3,1);
    graph.addEdge(2,4,7);
    graph.addEdge(3,5,3);
    graph.addEdge(5,6,5);
    graph.addEdge(4,6,1);
    graph.addEdge(5,4,2);

    Integer source = 1;
    Map<Integer,NodePathMinWeight<Integer>> map = dijkstra(graph,source);
    map.forEach((k,v) -> {
        v.path.add(0,source);
        System.out.println("source vertex :["+source+"] to vertex :["+k+"] cost:"+v.minCost+" shortest path :"+v.path);
    });
}

}

Output-:

source vertex :[1] to vertex :[2] cost:2 shortest path :[1, 2]

source vertex :[1] to vertex :[3] cost:3 shortest path :[1, 2, 3]

source vertex :[1] to vertex :[4] cost:8 shortest path :[1, 2, 3, 5, 4]

source vertex :[1] to vertex :[5] cost:6 shortest path :[1, 2, 3, 5]

source vertex :[1] to vertex :[6] cost:9 shortest path :[1, 2, 3, 5, 4, 6]

Rosariarosario answered 26/5, 2021 at 14:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.