package DataStructures;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import java.io.*;
/**
*
A class implementing a weighted directed graph and shortest path
* algorithms for a positively weighted graph.
*
* Graphs are created empty
*
* WeightedGraph g = new WeightedGraph();
*
* after which weighted directed arcs can be added by
*
* g.addArc(tail, head, weight)
*
* where tail is the String label of the source node of the arc,
* head is the String label of the destination node of the arc
* and weight is an integer.
*
* Weighted arcs can iterated over, for example by
*
* for (WeightedGraph.Arc a : g) {
* System.out.println(a);
* }
*
* Nodes can be iterated over, for example by
*
* for (Iterator i = g.nodes(); i.hasNext();) {
* System.out.println(i.next());
* }
*
*
* Graph descriptions can be read from and written to files.
*
* @see DataStructures.WeightedGraph.Arc
*
* @author Peter Williams
*/
public class WeightedGraph implements Iterable {
protected LookupTable nodeMap; // for mapping node labels to nodes
protected int version; // changes whenever the graph changes
private int nrArcs; // the number of arcs in the graph
/**
* Constructs an empty weighted graph
*/
public WeightedGraph() {
nodeMap = new DoubleHashingTable();
nrArcs = 0;
version = 0;
}
/**
* Tests whether a node is present in the graph.
* @param label the label of the node to be searched for.
* @return true if the node is present,
* false otherwise
*/
public boolean contains(String label) {
return nodeMap.contains(label);
}
/**
* Returns the number of nodes in the graph
* @return the number of nodes
*/
public int nrNodes() {
return nodeMap.size();
}
/**
* Returns the number of arcs in the graph
* @return the number of arcs
*/
public int nrArcs() {
return nrArcs;
}
// the basic container for a node
protected static class Node {
String label; // the label of this node
LinkedList adjacent; // list of arcs from here to adjacent nodes
Object work; // for workspace in applications
Node(String label) {
this.label = label;
adjacent = new LinkedList();
work = null;
}
}
// represents an arc out of a given unspecified node
protected static class Edge {
Node tail; // the source node
Node head; // the destination node
int weight; // the weight on this arc
Edge(Node tail, Node head, int weight) {
this.tail = tail;
this.head = head;
this.weight = weight;
}
}
/**
* Adds a node into the graph.
* @param label the label of the node
*/
public void addNode(String label) {
try {
nodeMap.lookup(label);
} catch (NoSuchElementException e) {
nodeMap.add(label, new Node(label));
++version;
}
}
// returns the node if it already exists, otherwise a new node with this label
private Node getNode(String label) {
Node node;
try {
node = nodeMap.lookup(label);
} catch (NoSuchElementException e) {
node = new Node(label);
nodeMap.add(label, node);
++version;
}
return node;
}
/**
* Adds a weighted directed arc into the graph.
* @param tail the label of the source node
* @param head the label of the destination node and
* @param weight the weight attached to the arc.
*/
public void addArc(String tail, String head, int weight) {
Node t = getNode(tail);
Node h = getNode(head);
t.adjacent.addFirst(new Edge(t, h, weight));
++nrArcs;
++version;
}
// protected iterator over edges
protected Iterator edges() {
return new Iterator () {
private Iterator>
nodes = nodeMap.iterator();
private final int n = nrArcs; // a copy in case we remove some arcs
private int count = 0; // to test whether we have finished
public boolean hasNext() {
return count < n;
}
private Iterator i = new EmptyIterator();
public Edge next() {
if (count < n) {
// skip to the next node with outgoing arcs
while (!i.hasNext()) {
i = nodes.next().value().adjacent.iterator();
}
++count;
return i.next();
} else {
throw new NoSuchElementException();
}
}
public void remove() {
i.remove();
--nrArcs;
++version;
}
};
}
/**
* Returns an iteration over the nodes in the graph.
* @return an iteration over the nodes in the graph
*/
public Iterator nodes() {
return new Iterator () {
private Iterator>
i = nodeMap.iterator();
private Node current = null; // saved in case of removal
public boolean hasNext() {
return i.hasNext();
}
public String next() {
current = i.next().value();
return current.label;
}
public void remove() {
// remove all incoming arcs
Iterator e = edges();
while (e.hasNext()) {
if (e.next().head == current) {
e.remove();
}
}
// remove the node and all outgoing arcs
nrArcs -= current.adjacent.size();
i.remove();
++version;
}
};
}
/**
* A public class returned by WeightedGraph.arcs().
* An arc is represented as a
* (tail, head, weight) triple,
* where tail and head are string node labels and
* weight is an integer.
* @see WeightedGraph#arcs
*/
public static class Arc {
private String tail;
private String head;
private int weight;
private Arc(String tail, String head, int weight) {
this.tail = tail;
this.head = head;
this.weight = weight;
}
/**
* Returns the tail or source of this arc.
* @return the tail of the arc.
*/
public String tail() {
return tail;
}
/**
* Returns the head or destination of this arc.
* @return the head of the arc.
*/
public String head() {
return head;
}
/**
* Returns the weight attached to this arc.
* @return the weight of the arc.
*/
public int weight() {
return weight;
}
/**
* Represents the arc as a string.
* @return the string representation.
*/
public String toString() {
return tail + " " + head + " " + weight;
}
}
/**
* Returns an iteration over the arcs in the graph. The next method
* returns an element of the WeightedGraph.Arc class.
* @see Arc
* @return an iteration over the arcs in the graph
*/
public Iterator arcs() {
return new Iterator () {
private Iterator i = edges();
public boolean hasNext() {
return i.hasNext();
}
public Arc next() {
Edge e = i.next();
return new Arc(e.tail.label, e.head.label, e.weight);
}
public void remove() {
i.remove();
}
};
}
/**
* Returns the arcs iterator
* @see WeightedGraph#arcs
* @return the iterator.
*/
public Iterator iterator() {
return arcs();
}
// workspace for each node for single-source shortest path algorithm
private static class WorkSpace {
boolean settled; // whether the shortest route to this node is known
boolean visited; // true if a settled node or adjacent to a settled node
int distance; // shortest known distance to this node from source
Node previous; // previous node on shortest path
}
/**
* Tests whether a path exists between two given nodes.
* @param origin the node at which to begin,
* @param destination the node at which to end.
* @return true if a path exists, false otherwise.
* @exception NoSuchElementException if either node does not exist.
* @exception ArithmeticException if a negatively weighted arc is
* encountered in the search.
*/
public boolean pathExists(String origin, String destination) {
Node source = nodeMap.lookup(origin);
Node target = nodeMap.lookup(destination);
dijkstra(source);
return ((WorkSpace) target.work).settled;
}
/**
* Returns the length of a shortest path between two given nodes.
* @param origin the node at which to begin,
* @param destination the node at which to end.
* @return the shortest path length.
* @exception NoSuchElementException if either node does not exist,
* or if there is no path between them.
* @exception ArithmeticException if a negatively weighted arc is
* encountered in the search.
*/
public int shortestPathLength(String origin, String destination) {
Node source = nodeMap.lookup(origin);
Node target = nodeMap.lookup(destination);
dijkstra(source);
if (((WorkSpace) target.work).settled) {
return ((WorkSpace) target.work).distance;
} else {
throw new NoSuchElementException();
}
}
/**
* Enumerates the nodes in a shortest path between two given nodes.
* @param origin the node at which to begin,
* @param destination the node at which to end.
* @return the enumeration.
* @exception NoSuchElementException if either node does not exist,
* or if there is no path between them.
* @exception ArithmeticException if a negatively weighted arc is
* encountered in the search.
*/
public Enumeration shortestPath(String origin, String destination) {
final Node source = nodeMap.lookup(origin);
final Node target = nodeMap.lookup(destination);
dijkstra(source);
if (!((WorkSpace) target.work).settled) {
throw new NoSuchElementException();
}
return new Enumeration() {
// use a stack to reverse the order
private Stack s = new StackArray();
private Node current = target;
{ // load the stack
while (current != source) {
s.push(current);
current = ((WorkSpace) current.work).previous;
}
s.push(source);
}
public boolean hasMoreElements() {
return !s.isEmpty();
}
public String nextElement() {
if (!s.isEmpty()) {
return s.pop().label;
} else {
throw new NoSuchElementException();
}
}
};
}
// items for the binary heap used in Dijkstra's algorithm
private static class HeapItem implements Comparable {
Node node;
int distance;
HeapItem(Node node, int distance) {
this.node = node;
this.distance = distance;
}
// highest priority given to lowest weight
public int compareTo(HeapItem other) {
return other.distance - distance;
}
}
private Node lastSource = null; // source when dijkstra was last called
private int lastVersion = 0; // version when dijkstra was last called
// the basic algorithm
private void dijkstra(Node source) {
// don't run if nothing has changed
if (version == lastVersion && source == lastSource) {
return;
}
// allocate new work space if necessary
if (version != lastVersion) {
for (LookupTable.Entry e : nodeMap) {
Node v = e.value();
if (v.work == null) {
v.work = new WorkSpace();
}
}
}
// solve the single-source all-paths problem
// initialise
for (LookupTable.Entry e : nodeMap) {
Node v = e.value();
((WorkSpace) v.work).visited = false;
((WorkSpace) v.work).settled = false;
}
((WorkSpace) source.work).visited = true;
((WorkSpace) source.work).distance = 0;
// find the shortest paths
PriorityQueue q = new BinaryHeap();
q.add(new HeapItem(source, 0));
while (!q.isEmpty()) {
Node v = q.remove().node;
WorkSpace vv = (WorkSpace) v.work;
if (!vv.settled) {
vv.settled = true;
for (Edge e : v.adjacent) {
Node w = e.head;
int c = e.weight;
if (c < 0) {
throw new ArithmeticException("Negative weight in graph");
}
int d = vv.distance + c;
WorkSpace ww = (WorkSpace) w.work;
if (!ww.visited || ww.distance > d) {
ww.visited = true;
ww.distance = d;
ww.previous = v;
q.add(new HeapItem(w, d));
}
}
}
}
lastVersion = version;
lastSource = source;
}
/**
* Reads a graph description from a text file.
* Entries read from the file are added to the graph.
* File entries must be of form
*
* T H W
*
* where T and H are strings and W is an integer. Each entry
* represents a weighted arc where T is the tail or source node,
* H is the head or destination node and W is the weight.
* All entries must be on separate lines. Empty lines are skipped.
*/
public void readGraphTxtFile(String fileName) {
try {
BufferedReader inStream =
new BufferedReader(new FileReader(fileName));
String line;
while ((line = inStream.readLine()) != null) {
if (line.length() > 0) {
StringTokenizer s = new StringTokenizer(line);
try {
if (s.countTokens() == 3) {
String tail = s.nextToken();
String head = s.nextToken();
int weight = Integer.parseInt(s.nextToken());
addArc(tail, head, weight);
} else {
throw new Exception();
}
} catch (Exception e) {
System.err.println("error in graph file entry: " + line);
System.exit(1);
}
}
}
inStream.close();
} catch (FileNotFoundException e) {
System.err.println("cannot open " + fileName);
System.exit(1);
} catch (IOException e) {
System.err.println("error: " + e);
System.exit(1);
}
}
/**
* Writes a graph description to a text file.
* Files are written in the same form in which
* readGraphTxtFile expects to read them.
* Note: it is the applications's responsibility to protect files
* which ought not to be overwritten.
*/
public void writeGraphTxtFile(String fileName) {
try {
BufferedWriter outStream =
new BufferedWriter(new FileWriter(fileName));
for (Arc a : this) {
outStream.write(a.toString());
outStream.newLine();
}
outStream.close();
} catch (FileNotFoundException e) {
System.err.println("cannot open " + fileName);
System.exit(1);
} catch (IOException e) {
System.err.println("error: " + e);
}
}
}