0

I'm trying to implement a BFS to find all prerequisites required before a certain course can be taken. My public List<Node> computeAllPrereqs(String courseName) method is where my code is messing up. Can someone look at my method and help me find the issue? I figured the finish node would be none, because my the courses in my graph all lead to none. The graph is not cyclic.

This is the text file that I'm passing into the constructor, the first column is the course and followed by the course are its prerequisites:

CS1 None
CS2 CS1
CS3 CS2
CS4 CS1
CS5 CS3 CS4
CS6 CS2 CS4

.

public class Graph {

    private Map<String, Node> graph;

    public Graph(String filename) throws FileNotFoundException {

        // open the file for scanning
        File file = new File(filename);
        Scanner in = new Scanner(file);

        // create the graph
        graph = new HashMap<String, Node>();

        // loop over and parse each line in the input file
        while (in.hasNextLine()) {
            // read and split the line into an array of strings
            // where each string is separated by a space.
            Node n1, n2;
            String line = in.nextLine();
            String[] fields = line.split(" ");
            for(String name: fields){
                if (!graph.containsKey(fields[0]))
                {
                    n1 = new Node(name);
                    graph.put(name, n1);
                }
                else{
                    n2 = new Node(name);
                    graph.get(fields[0]).addNeighbor(n2);
                }
            }
        }
        in.close();
    }    
    public List<Node> computeAllPrereqs(String courseName){
        // assumes input check occurs previously
        Node startNode;
        startNode = graph.get(courseName);


        // prime the dispenser (queue) with the starting node
        List<Node> dispenser = new LinkedList<Node>();
        dispenser.add(startNode);

        // construct the predecessors data structure
        Map<Node, Node> predecessors = new HashMap<Node,Node>();
        // put the starting node in, and just assign itself as predecessor
        predecessors.put(startNode, startNode);

        // loop until either the finish node is found, or the
        // dispenser is empty (no path)
        while (!dispenser.isEmpty()) {
            Node current = dispenser.remove(0);
            if (current == new Node(null)) {
                break;
            }
            // loop over all neighbors of current
            for (Node nbr : current.getNeighbors()) {
                // process unvisited neighbors
                if(!predecessors.containsKey(nbr)) {
                    predecessors.put(nbr, current);
                    dispenser.add(nbr);
                }
            }
        }

        return constructPath(predecessors, startNode, null);
    }
    private List<Node> constructPath(Map<Node,Node> predecessors,
                                     Node startNode, Node finishNode) {

        // use predecessors to work backwards from finish to start,
        // all the while dumping everything into a linked list
        List<Node> path = new LinkedList<Node>();

        if(predecessors.containsKey(finishNode)) {
            Node currNode = finishNode;
            while (currNode != startNode) {
                path.add(0, currNode);
                currNode = predecessors.get(currNode);
            }
            path.add(0, startNode);
        }

        return path;
    }

}

Here's the node class I used to make the nodes and neighbors in the graph:

public class Node {

    /*
     *  Name associated with this node.
     */
    private String name;

    /*
     * Neighbors of this node are stored as a list (adjacency list).
     */
    private List<Node> neighbors;


    public Node(String name) {
        this.name = name;
        this.neighbors = new LinkedList<Node>();
    }


    public String getName() {
        return name;
    }

    public void addNeighbor(Node n) {
        if(!neighbors.contains(n)) {
            neighbors.add(n);
        }
    }

    public List<Node> getNeighbors() {
        return new LinkedList<Node>(neighbors);
    }


    @Override
    public String toString() {
        String result;
        result = name + ":  ";

        for(Node nbr : neighbors) {
            result = result + nbr.getName() + ", ";
        }
        // remove last comma and space, or just spaces in the case of no neighbors
        return (result.substring(0, result.length()-2));
    }


    @Override
    public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof Node) {
            Node n = (Node) other;
            result = this.name.equals(n.name);
        }
        return result;
    }

    @Override
    public int hashCode() {
        return this.name.hashCode();
    }
}

Here's my test class:

public class Prerequisite {

/**
 * Main method for the driver program.
 *
 * @param args the name of the file containing the course and
 * prerequisite information
 *
 * @throws FileNotFoundException if input file not found
 */
public static void main(String[] args) throws FileNotFoundException {

    // Check for correct number of arguments
    if(args.length != 1) {
        String us = "Usage: java Prerequisite <input file>";
        System.out.println(us);
        return;
    }

    // create a new graph and load the information
    // Graph constructor from lecture notes should
    // be modified to handle input specifications
    // for this lab.
    Graph graph = new Graph(args[0]);

    // print out the graph information
    System.out.println("Courses and Prerequisites");
    System.out.println("=========================");
    System.out.println(graph);


    // ASSUMPTION:  we assume there are no cycles in the graph

    // Part I:
    // compute how many (and which) courses must be taken before it is
    // possible to take any particular course

    System.out.println("How many courses must I take "
            + "before a given course?!?!?");
    for(String name : graph.getAllCourseNames()) {
        List<Node> allPrereqs = graph.computeAllPrereqs(name);
        System.out.print(String.valueOf(allPrereqs.size()));
        System.out.print(" courses must be taken before " + name + ": ");
        for(Node el : allPrereqs) {
            System.out.print(el.getName() + " ");
        }
        System.out.println();
    }


}

When I run this test my output is:

0 courses must be taken before CS1: 
0 courses must be taken before CS3: 
0 courses must be taken before CS2: 
0 courses must be taken before CS5: 
0 courses must be taken before CS4: 
0 courses must be taken before CS6: 

it should be:

0 courses must be taken before CS1: 
2 courses must be taken before CS3: CS1 CS2 
1 courses must be taken before CS2: CS1 
4 courses must be taken before CS5: CS1 CS3 CS2 CS4 
1 courses must be taken before CS4: CS1 
3 courses must be taken before CS6: CS1 CS2 CS4 

I know I'm posting a lot of code but I don't want to have to edit in more code in later if it is needed to help fix my bug.

chengpohi
  • 14,064
  • 1
  • 24
  • 42
FatFockFrank
  • 169
  • 1
  • 4
  • 17

2 Answers2

2

As a note, prerequisites are more effectively determined with depth first search (DFS) such that a topological ordering can be realized.

During graph construction, when linking neighbors, "lookalikes" are being linked rather than existing graph nodes themselves, so the resulting graph is actually unconnected. To resolve that issue, link the actual nodes of the graph to each other.

            if (!graph.containsKey(fields[0]))
            {
                n1 = new Node(name);
                graph.put(name, n1);
            }
            else{
                if(graph.containsKey(name)) {
                    n2 = graph.get(name);
                }
                else {
                    n2 = new Node(name);
                    graph.put(name, n2);
                }
                graph.get(fields[0]).addNeighbor(n2);
            }

An added benefit of the above code snippet is that it adds the terminal "None" node to the graph.

A single line path can't be constructed because a course may have more than one prerequisite. For instance, CS6 depends on both CS2 and CS4. In the constructPath method, the terminal condition would fire after only following one of those paths. So given the current structure of the program, about the best you can achieve is to output the set of prerequisite courses rather than the single line path.

private List<Node> constructPath(Map<Node,Node> predecessors,
                                 Node startNode, Node finishNode) {
/*
    // use predecessors to work backwards from finish to start,
    // all the while dumping everything into a linked list
    List<Node> path = new LinkedList<Node>();

    if(predecessors.containsKey(finishNode)) {
        Node currNode = finishNode;
        while (currNode != startNode) {
            path.add(0, currNode);
            currNode = predecessors.get(currNode);
        }
        path.add(0, startNode);
    }
*/
    Set<Node> prereqs = new HashSet<Node>(predecessors.keySet());
    prereqs.remove(graph.get("None"));
    return new ArrayList<Node>(prereqs);
}

Given this approach, neither arguments startNode nor finishNode are necessary and the creation of the predecessor map is redundant.

Lastly, a course is not a prerequisite of itself so it's incorrect to assign itself as a predecessor.

    // put the starting node in, and just assign itself as predecessor
    // predecessors.put(startNode, startNode);

Given these modifications, this was the output

0 courses must be taken before CS1: 
1 courses must be taken before CS2: CS1 
2 courses must be taken before CS3: CS2 CS1 
1 courses must be taken before CS4: CS1 
4 courses must be taken before CS5: CS4 CS2 CS3 CS1 
3 courses must be taken before CS6: CS4 CS2 CS1 

To improve on the code, a TreeSet (http://docs.oracle.com/javase/7/docs/api/java/util/TreeSet.html) can be used instead of a HashSet to output the prerequisites in a uniform order. To use TreeSet however, the Node class will have to be augmented to implement Comparable (How to implement the Java comparable interface?).

And again, if outputting the set of prerequisites is not satisfactory, consider using DFS instead to generate a topological ordering (http://www.personal.kent.edu/~rmuhamma/Algorithms/MyAlgorithms/GraphAlgor/topoSort.htm).

Community
  • 1
  • 1
chiaboy
  • 394
  • 2
  • 6
0
if (current == new Node(null)) {
        break;
    }

You should have used continue instead of break. Even if you have encountered null-node, you can have more normal nodes on the queue, which have longer path to the null-node.

You can realize it when you analyze graph starting from CS5

bartexsz
  • 239
  • 1
  • 11