Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementation of BFS, DFS and Dijkstra

Is it true that the implementation of BFS, DFS and Dijkstra are almost the same, except that BFS uses queue, DFS uses stack, while Dijkstra uses min priority queue?

More precisely. Can we use the following code for all of BFS, DFS, and Dijkstra, with Q being a queue for BFS, and a stack for DFS, and a min priority queue for Dijkstra? Thanks!

Init d[]=Inf; // distance from the node s
Init c[]='w'; // color of nodes: 'w':undiscovered, 'g':discovered, 'b':fully explored
Init p[]=null; // previous node in the path
c[s]='g';
d[s]=0;
Q.push(s);
while(!Q.empty()) {
    u = Q.front();
    Q.pop();
    for v in adj[u] {
        if(c(v)=='w') {
            c[v]='g';
            if(d[u]+w(u,v)<d[v]) {
                d[v]=d[u]+w(u,v);
                p[v]=u;
            }
            Q.push(v);
        }
    }
    c[u]='b';
}
like image 947
Chong Luo Avatar asked Jan 05 '12 19:01

Chong Luo


People also ask

How BFS and DFS is implemented?

BFS is a traversal technique in which all the nodes of the same level are explored first, and then we move to the next level. DFS is also a traversal technique in which traversal is started from the root node and explore the nodes as far as possible until we reach the node that has no unvisited adjacent nodes.

How is DFS implemented?

This recursive nature of DFS can be implemented using stacks. The basic idea is as follows: Pick a starting node and push all its adjacent nodes into a stack. Pop a node from stack to select the next node to visit and push all its adjacent nodes into a stack.

What is DFS and how is it implemented?

Depth-first search (DFS), is an algorithm for tree traversal on graph or tree data structures. It can be implemented easily using recursion and data structures like dictionaries and sets.


2 Answers

Yes

Let's say we have this graph, and want to find the shortest distances starting at A:

Graph

Here is a simple NodeCollection interface that allows for the operations needed for the traversal:

interface NodeCollection<E> {
    void offer(E node);
    E extract();
    boolean isEmpty();
}

And the implementations for queue, stack and priority queue. Note that this interface and classes don't really need to be generic:

static class NodeQueue<E> implements NodeCollection<E> {
    private final Queue<E> queue = new LinkedList<E>();
    @Override public void offer(E node) { queue.offer(node); }
    @Override public E extract() { return queue.poll(); }
    @Override public boolean isEmpty() { return queue.isEmpty(); }
}

static class NodeStack<E> implements NodeCollection<E> {
    private final Stack<E> stack = new Stack<E>();
    @Override public void offer(E node) { stack.push(node); }
    @Override public E extract() { return stack.pop(); }
    @Override public boolean isEmpty() { return stack.isEmpty(); }
}

static class NodePriorityQueue<E> implements NodeCollection<E> {
    private final PriorityQueue<E> pq = new PriorityQueue<E>();
    @Override public void offer(E node) { pq.add(node); }
    @Override public E extract() { return pq.poll(); }
    @Override public boolean isEmpty() { return pq.isEmpty(); }
}

Note that for PriorityQueue to work as expected, the Node class needs to provide a compareTo(Node) method:

static class Node implements Comparable<Node> {
    final String name;
    Map<Node, Integer> neighbors;
    int dist = Integer.MAX_VALUE;
    Node prev = null;
    char color = 'w';

    Node(String name) {
        this.name = name;
        this.neighbors = Maps.newHashMap();
    }

    @Override public int compareTo(Node o) {
        return ComparisonChain.start().compare(this.dist, o.dist).result();
    }
}

Now here's the Graph class. Note that the traverse method takes a NodeCollection instance, which will be used for storing nodes during the traversal.

static class Graph {
    Map<String, Node> nodes = Maps.newHashMap();

    void addEdge(String fromName, String toName, int weight) {
        Node from = getOrCreate(fromName);
        Node to = getOrCreate(toName);
        from.neighbors.put(to, weight);
        to.neighbors.put(from, weight);
    }

    Node getOrCreate(String name) {
        if (!nodes.containsKey(name)) {
            nodes.put(name, new Node(name));
        }
        return nodes.get(name);
    }

    /**
     * Traverses this graph starting at the given node and returns a map of shortest paths from the start node to
     * every node.
     *
     * @param startName start node
     * @return shortest path for each node in the graph
     */
    public Map<String, Integer> traverse(String startName, NodeCollection<Node> collection) {
        assert collection.isEmpty();
        resetNodes();

        Node start = getOrCreate(startName);
        start.dist = 0;
        collection.offer(start);

        while (!collection.isEmpty()) {
            Node curr = collection.extract();
            curr.color = 'g';
            for (Node neighbor : curr.neighbors.keySet()) {
                if (neighbor.color == 'w') {
                    int thisPathDistance = curr.dist + curr.neighbors.get(neighbor);
                    if (thisPathDistance < neighbor.dist) {
                        neighbor.dist = thisPathDistance;
                        neighbor.prev = curr;
                    }
                    collection.offer(neighbor);
                }
            }
            curr.color = 'b';
        }

        Map<String, Integer> shortestDists = Maps.newTreeMap();
        for (Node node : nodes.values()) {
            shortestDists.put(node.name, node.dist);
        }
        return shortestDists;
    }

    private void resetNodes() {
        for (Node node : nodes.values()) {
            node.dist = Integer.MAX_VALUE;
            node.prev = null;
            node.color = 'w';
        }
    }
}

Finally here's the main method, which traverses the same graph 3 times, once with each of the NodeCollection types:

private static Graph initGraph() {
    Graph graph = new Graph();
    graph.addEdge("A", "B", 2);
    graph.addEdge("B", "C", 2);
    graph.addEdge("C", "D", 2);
    graph.addEdge("D", "E", 2);
    graph.addEdge("E", "F", 2);
    graph.addEdge("F", "L", 2);

    graph.addEdge("A", "G", 10);
    graph.addEdge("G", "H", 10);
    graph.addEdge("H", "I", 10);
    graph.addEdge("I", "J", 10);
    graph.addEdge("J", "K", 10);
    graph.addEdge("K", "L", 10);

    return graph;
}

public static void main(String[] args) {
    Graph graph = initGraph();
    System.out.println("Queue (BFS):\n" + graph.traverse("A", new NodeQueue<Node>()));
    System.out.println("Stack (DFS):\n" + graph.traverse("A", new NodeStack<Node>()));
    System.out.println("PriorityQueue (Dijkstra):\n" + graph.traverse("A", new NodePriorityQueue<Node>()));
}

And the results!

Queue (BFS):
{A=0, B=2, C=4, D=6, E=8, F=10, G=10, H=20, I=30, J=40, K=22, L=12}
Stack (DFS):
{A=0, B=2, C=4, D=66, E=64, F=62, G=10, H=20, I=30, J=40, K=50, L=60}
PriorityQueue (Dijkstra):
{A=0, B=2, C=4, D=6, E=8, F=10, G=10, H=20, I=30, J=32, K=22, L=12}

Note that DFS will sometimes take the top branch first, yielding different but symmetric results.

Here's what the results look like this on the graph:

Results

like image 145
oksayt Avatar answered Sep 19 '22 14:09

oksayt


On the matter of BFS vs. DFS: yes and no, but more "no" than "yes".

If all you care about is the forward traversal order, i.e. the order in which the algorithm discovers the new vertices of the graph, then yes: you can take the classic BFS algorithm, replace the FIFO queue with LIFO stack, and you will get pseudo-DFS algorithm.

However, I call it pseudo-DFS algorithm because it is not really the same as the classic DFS.

The DFS algorithm obtained that way will indeed generate genuine DFS vertex discovery order. However, it will still be different from the classic DFS in some other regadrs. You can find the description of the classic DFS in any book on algorithms (or Wikipedia) and you will see that the structure of the algorithm is notably different from BFS. It is done that way for a reason. The classic DFS offers some additional benefits besides producing the proper DFS vertex discovery order. These additional benefits include

  • Lower peak memory consumption. In classic DFS implementation the stack size at each moment in time equals the length of the path from the search origin to the current vertex. In pseudo-DFS the stack size at each moment in time equals the sum of the degrees of all vertices from the search origin to the current vertex. This means that the peak memory consumption of pseudo-DFS algorithm will potentially be considerably higher.

    For an extreme example, consider a "snowflake" graph consisting of a single vertex in the center directly connected to a 1000 vertices surrounding it. A classic DFS will traverse this graph with maximum stack depth of 1. Meanwhile, a pseudo-DFS will start by pushing all 1000 vertices into the stack (in BFS fashion), resulting in peak stack depth of 1000. That's quite a difference.

  • Backtracking. A classic DFS algorithm is a genuine recursive algorithm. As a recursive algorithm in addition to the forward traversal order (i.e. vertex discovery order), it also provides you with backward traversal order (backtracking). In the classic DFS you visit each vertex multiple times: first time when you discover it for the very first time, then when you return back from one of its descendant vertices to proceed to the next descendant vertex, and finally for the very last time when you have processed all of its descendants. Many DFS-based algorithms are build on catching and handling these visits. For example, topological sorting algorithm is classic DFS that outputs the vertices in the order of their last DFS visit. The pseudo-DFS algorithm, as I said above, only provides you with clear access to the first event (vertex discovery), but does not register any backtracking events.

like image 35
AnT Avatar answered Sep 22 '22 14:09

AnT