Download Appendix B JAVA GRAPH ADT

Survey
yes no Was this document useful for you?
   Thank you for your participation!

* Your assessment is very important for improving the work of artificial intelligence, which forms the content of this project

Document related concepts
no text concepts found
Transcript
Appendix B
JAVA GRAPH ADT
This appendix, written by Dr. Michael Dinneen in 1999, presents a simplified abstract
class for representing a graph abstract data type (ADT). Although it is fully functional,
it purposely omits most exception handling and other niceties that should be in any commercial level package. These details would distract from our overall (introductory) goal
of showing how to implement a basic graph class in Java.
Our plan is to have a common data structure that represents both graphs and digraphs.
A graph will be a digraph with anti-parallel arcs; that is, if (u; v ) 2 E then (v; u) 2 E
also. The initial abstract class presented below requires a core set of methods needed
for the realized graph ADT. It will be extended with the actual internal data structure
representation in the form of adjacency matrix or adjacency lists (or whatever the designer
picks).
import java.util.Vector;
import java.io.BufferedReader;
/*
* Current Abstract Data Type interface for (di)graph classes.
*/
abstract class GraphADT
{
// Need default constructor, copy and BufferedReader constructors
// (commented since java doesn’t allow abstract constructors!)
//
// public GraphADT();
// public GraphADT(GraphADT);
// public GraphADT(BufferedReader in);
Right from the beginning we get in trouble since Java does not allow abstract constructors. We will leave these as comments and hope the graph class designer will abide by
them. We want to create graphs from an empty graph, copy an existing graph, or read in
233
234
COMPSCI.220FT
one from some external source. In the case of a BufferedReader constructor the user
has to attach one to a string, file or keyboard. We will see examples later.
We now proceed with the graph class interface.
// data structure modifiers
//
abstract public void addVertices(int i);
abstract public void removeVertex(int i);
abstract public void addArc(int i, int j);
public void addEdge(int i, int j)
{ addArc(i,j); addArc(j,i); }
//
//
//
//
Add some vertices
Remove vertex
Add directed edge
Add undirected edge
abstract public void removeArc(int i, int j); // Remove directed edge
public void removeEdge(int i, int j) // Remove undirected edge
{ removeArc(i,j); removeArc(j,i);
}
This small set of methods allows one to build the graph. Notice how we explicitly define
the methods for adding or deleting edges in terms of the two arc methods. An extended
class can override these to improve efficiency if it wants. We now list a few methods for
extracting information from a graph object.
// data structure queries
//
abstract public boolean isArc(int i, int j); // Check for arcs
public boolean isEdge(int i, int j) // Check for edges
{ return isArc(i,j) && isArc(j,i); }
abstract public int inDegree(int i);
abstract public int outDegree(int i);
public int degree(int i)
{ return outDegree(i); }
// indegree
// outdegree
// Number of neighbours
abstract public Vector neighbors(int i);
abstract public int order();
abstract public int size();
// List of (out-) neighbours
// Number of vertices
// Number of edges
Again, we define the isEdge method in terms of the isArc method. We give a default
definition for the degree of a graph in terms of the out-degree. This is fine for graphs but
may not be for digraphs. For our implementation, we want the degree to equal the number
of vertices returned by the neighbors method.
One nice thing to offer is a method to view/save/print a graph. Traditionally in Java we
define a toString method for this.
COMPSCI.220FT
235
// output (default same as representation)
//
abstract public String toString();
// two common external text representations for graphs follow
//
public String toStringAdjMatrix()
{
StringBuffer o = new StringBuffer();
o.append(order()+"\n");
for( int i = 0; i < order(); i++ )
{
for( int j = 0; j < order(); j++ )
{
if ( isArc(i,j) ) o.append("1 ");
else o.append("0 ");
}
o.append("\n");
}
return o.toString();
}
public String toStringAdjLists()
{
StringBuffer o = new StringBuffer();
o.append(order()+"\n");
for( int i = 0; i < order(); i++ )
{
for( int j = 0; j < order(); j++ )
{
if ( isArc(i,j) ) o.append(j+" ");
}
o.append("\n");
}
return o.toString();
}
} // end of class GraphADT
Notice how we went ahead and defined both the adjacency matrix and adjacency lists
output methods. We left the toString method as an abstract method for the derived
classes to define. We want a BufferedReader constructor for a graph class to accept
its own toString output.
To make things convenient for ourselves we require that the first line of our (two) external
236
COMPSCI.220FT
graph representations contain the number of vertices. Strictly speaking, this is not needed
for an 0/1 adjacency matrix. This makes our parsing job easier and this format allows us
to store more than one graph per input stream. (We can terminate a stream of graphs with
a sentinel graph of order zero.)
B.1 Java adjacency matrix implementation
We now define our first usable graph class based on an adjacency matrix representation
(for graphs and digraphs). This class extends our graph interface GraphADT.
import java.io.*;
import java.util.*;
/* Current implementation uses adjacency matrix form of a graph.
*/
public class GraphAdjMatrix extends GraphADT
{
// --------------------------------------------------------------// Internal Representation and Constructors
// ---------------------------------------------------------------
private int _space;
private int _order;
private boolean _adj[][];
// Current space allocated.
// Number of vertices.
// Adjacency matrix of graph.
private static boolean _allocate(int n)[][]
{
return new boolean[n][n];
}
// Free store routines.
public GraphAdjMatrix() // default constructor
{
_space = _order = 0;
}
public GraphAdjMatrix(GraphAdjMatrix G) // copy constructor
{
int n = G.order();
if ( n>0 ) _adj = _allocate( n );
_space = _order = n;
for( int i = 0; i < n; i++ )
for( int j = 0; j < n; j++ )
_adj[i][j] = G._adj[i][j];
}
COMPSCI.220FT
237
We isolated a private method1 _allocate that gets memory for the adjacency matrix.
The default constructor simply creates an empty graph and thus there is no need to allocate any space. The copy constructor simply copies onto a new n-by-n matrix the boolean
values of the old graph. Notice that we want new storage and not an object reference for
the copy.
We keep an integer variable _space to represent the total space allocated. Whenever we
delete vertices we do not want to reallocate a new matrix but to reshuffle the entries into
the upper sub-matrix. Then whenever adding more vertices we just extend the dimension
of the sub-matrix.
public GraphAdjMatrix(BufferedReader buffer)
{
try {
String line=buffer.readLine();
StringTokenizer token = new StringTokenizer(line);
if ( token.countTokens() != 1 )
throw new Error("bad format: number of vertices");
int n = Integer.parseInt(token.nextToken());
if ( n>0 ) _adj = _allocate( n );
_order = _space = n;
for( int i = 0; i < n; i++ )
{
line = buffer.readLine();
token = new StringTokenizer(line);
if ( token.countTokens() != n )
throw new Error("bad format: adjacency matrix");
for( int j = 0; j < n; j++ )
{
int entry = Integer.parseInt( token.nextToken() );
_adj[i][j] = ( entry != 0 );
}
}
} catch (IOException x) {
throw new Error("bad input stream");
}
}
We have tried to minimize the complexity of this BufferedReader constructor. We
1
We use the programming convention of beginning private variables and methods with the underscore
character.
238
COMPSCI.220FT
do however throw a couple of errors if something does go wrong. Otherwise, this method
simply reads in an integer n denoting the dimension of the adjacency matrix and then
reads in the 0/1 matrix. Notice how the use of the StringTokenizer class makes our
task easy.
We next define several methods for altering this graph data structure.
// --------------------------------------------------------------// Mutator Methods
// --------------------------------------------------------------public void addVertices(int n)
{
if ( n > _space - _order )
{
boolean matrix[][] = _allocate( _order + n );
for( int i = 0; i < _order; i++ )
{
for( int j = 0; j < _order; j++ )
matrix[i][j] = _adj[i][j];
}
_adj = matrix;
_space = _order + n;
}
else
// expand and reclean matrix
{
for( int i = 0; i < _order + n; i++ )
{
for( int j = _order; j < _order + n; j++ )
_adj[i][j] = _adj[j][i] = false;
}
}
_order += n;
}
public void removeVertex(int v)
{
_order--;
int i;
for( i = 0; i < v; i++ )
{
for( int j = v; j < _order; j++ )
{
COMPSCI.220FT
239
_adj[i][j] = _adj[i][j + 1];
}
}
for( i = v; i < _order; i++ )
{
int j;
for( j = 0; j < v; j++ )
{
_adj[i][j] = _adj[i + 1][j];
}
for( j = v; j < _order; j++ )
{
_adj[i][j] = _adj[i + 1][j + 1];
}
}
}
These two methods allow the user to add or delete vertices from a graph. If we have already allocated enough space the addVertices method will simply expand the current
size (while initializing entries to false). Otherwise, a new larger matrix is allocated and
a copy is done.
The removeVertex method is somewhat complicated in that we have to remove a row
and column from the matrix corresponding to the vertex being deleted. We decided to
do this in two passes. The first pass (for variable i < v ) simply shifts all column indices
j v to the left. The second pass (for variable i v ) has to shift entries up by one while
also shifting column indices j v to the left. The user of the graph should realize that
the indices of the vertices change!
public void addArc(int i, int j)
{
_adj[i][j] = true;
}
public void removeArc(int i, int j)
{
_adj[i][j] = false;
}
Finally, above, we have two relatively trivial methods for adding and deleting arcs (and
edges). Recall that the add/remove edge methods of class GraphADT are defined in terms
of the arc methods.
The methods to access properties of the graph are also pretty straightforward.
240
COMPSCI.220FT
// --------------------------------------------------------------// Accessor Methods
// --------------------------------------------------------------public boolean isArc(int i, int j)
{
return _adj[i][j];
}
public int inDegree(int i)
{
int sz = 0;
for( int j = 0; j < _order; j++ ) if ( _adj[j][i] ) sz++;
return sz;
}
public int outDegree(int i)
{
int sz = 0;
for( int j = 0; j < _order; j++ ) if ( _adj[i][j] ) sz++;
return sz;
}
Our constant-time method for checking whether an arc is present in a graph is given above
in the method isArc. Unfortunately, we have to check all neighbors for computing the
in- and out- degrees. Also the method, given below, for returning a list of neighbors for a
vertex will need to scan all potential vertices.
public Vector neighbors(int i)
{
Vector nbrs = new Vector();
for (int j= 0; j<_order; j++)
{
if ( _adj[i][j] ) nbrs.addElement( new Integer(j) );
}
return nbrs;
}
public int order() { return _order; }
public int size()
// Number of arcs (edges count twice)
{
int sz = 0;
//
boolean undirected = true;
for( int i = 0; i < _order; i++ )
for( int j = 0; j < _order; j++ )
COMPSCI.220FT
241
{
if ( _adj[i][j] ) sz++;
if ( _adj[i][j] != _adj[j][i] ) undirected = false;
//
}
return sz;
// undirected ? sz / 2 : sz;
}
The order of the graph is stored in an integer variable _order. However, we have to
count all true entries in the boolean adjacency matrix to return the size. Notice that if
we are working with an undirected graph this returns twice the expected number (since we
store each edge as two arcs). If we specialize this class we may want to uncomment the
indicated statements to autodetect undirected graphs (whenever the matrix is symmetric).
It is probably safer to leave it as it is written, with the understanding that the user knows
how size is defined for this implementation of GraphADT.
// default output is readable by constructor
//
public String toString() { return toStringAdjMatrix(); }
} // end class GraphAdjMatrix
We finish our implementation by setting our output method toString to return an adjacency matrix. Recall the method toStringAdjMatrix was defined in the base class
GraphADT.
B.2 Java adjacency lists implementation
We now present an alternate implementation of our graph ADT using the adjacency lists
data structure. We will use the Java API class Vector to store these lists.
import java.io.*;
import java.util.*;
/* Current implementation uses adjacency lists form of a graph.
*/
public class GraphAdjLists extends GraphADT
{
// --------------------------------------------------------------// Internal Representation and Constructors
// --------------------------------------------------------------private Vector _adj;
// Vector of Vector of Integers
private void _allocate(int n)
242
COMPSCI.220FT
{
_adj = new Vector();
for( int i = 0; i < n; i++ ) _adj.addElement(new Vector());
}
public GraphAdjLists() // default constructor
{
_allocate(0);
}
public GraphAdjLists(GraphAdjLists G) // copy constructor
{
int n = G.order();
_adj = new Vector();
for( int i = 0; i < n; i++ ) _adj.addElement(G.neighbors(i));
}
We mimic the adjacency matrix code here with a private method to allocate memory.
The method _allocate creates a list of lists (i.e., a Vector of Vectors). Note the
created list, using new Vector(), within the argument of the addElement method.
The default constructor creates a list of no lists (i.e., no vertices). For better efficiency,
the copy constructor takes over the role of our allocator and appends the neighbor lists of
the graph parameter G directly onto a new adjacency list.2
public GraphAdjLists( BufferedReader buffer )
{
try {
String line = buffer.readLine();
StringTokenizer token = new StringTokenizer( line );
if ( token.countTokens() != 1 )
throw new Error("bad format: number of vertices");
int n = Integer.parseInt( token.nextToken() );
_allocate(n);
for( int u = 0; u < n; u++ )
{
line = buffer.readLine();
token = new StringTokenizer( line );
while ( token.hasMoreTokens() )
{
int v = Integer.parseInt( token.nextToken() );
((Vector) _adj.elementAt(u)).addElement(new Integer(v));
}
}
} catch (IOException x) {
throw new Error("bad input stream");
2
Yes, this is probably illustrating a bad programming style. Why?
COMPSCI.220FT
243
}
}
Our stream constructor reads in an integer denoting the order n of the graph and then
reads in n lines denoting the adjacency lists. Notice that we do not check for correctness
of the data. For example, a graph of 5 vertices could have erroneous adjacency lists
with numbers outside the range 0 to 4. We leave these robustness considerations for an
extended class to fulfil, if desired. Also note that we do not list the vertex index in front
of the individual lists and we use white space to separate items. A blank line indicates an
empty list (i.e., no neighbors) for a vertex.
// --------------------------------------------------------------// Mutator Methods
// --------------------------------------------------------------public void addVertices(int n)
{
if ( n > 0 )
{
for( int i = 0; i < n; i++)
_adj.addElement(new Vector());
}
}
public void removeVertex(int i)
{
_adj.removeElementAt(i);
Integer I = new Integer(i);
for( int u = 0; u < order(); u++ )
{
Vector uVec = (Vector) _adj.elementAt(u);
uVec.removeElement(I);
// remove i from
//
adjacency lists
for( int v = 0; v < uVec.size(); v++) // relabel larger
//
indexed nodes
{
int nbr = ((Integer) uVec.elementAt(v)).intValue();
if ( nbr > i )
uVec.setElementAt( new Integer( nbr - 1 ), v );
}
}
}
Adding vertices is easy for our adjacency lists representation. Here we just expand the
internal _adj list by appending new empty lists. The removeVertex method is a little
complicated in that we have to scan each list to remove arcs pointing to the vertex being
244
COMPSCI.220FT
deleted. We also have chosen to relabel vertices so that there are no gaps (i.e., we want
vertex indexed by i to be labeled Integer(i)). A good question would be to find a
more efficient removeVertex method. One way would be to also keep an in-neighbor
list for each vertex. However, the extra data structure overhead is not desirable for our
simple implementation.
public void addArc( int i, int j )
{
if ( isArc(i,j) ) return;
((Vector) _adj.elementAt(i)).addElement( new Integer(j ));
}
public void removeArc( int i, int j )
{
((Vector) _adj.elementAt(i)).removeElement( new Integer(j) );
}
Adding and removing arcs is easy since the methods to do this exist in the Vector class.
All we have to do is access the appropriate adjacency list. We have decided to place a
safe guard in the addArc method to prevent parallel arcs from being added between two
vertices.
// ------------------------------------------------------------// Accessor Methods
// --------------------------------------------------------------public boolean isArc( int i, int j )
{
return ((Vector)_adj.elementAt(i)).contains(new Integer(j));
}
public int inDegree(int i)
{
int sz = 0;
for( int j = 0; j < order(); j++ ) if ( isArc(j,i) ) sz++;
return sz;
}
public int outDegree(int i)
{
return ((Vector) _adj.elementAt(i)).size();
}
Note how we assume that the contains method of a Vector object does a data equality check and not just a reference check. The outDegree method probably runs in
constant time since we just return the list’s size. However, the inDegree method has to
check all adjacency lists and could have to inspect all arcs of the graph/digraph.
COMPSCI.220FT
245
public Vector neighbors( int i )
{
return (Vector)((Vector) _adj.elementAt(i)).clone();
}
public int order() { return _adj.size(); }
public int size()
// Number of arcs (counts edges twice).
{
int sz = 0;
for( int i = 0; i < order(); i++ )
{
sz += ((Vector) _adj.elementAt(i)).size();
}
return sz;
}
We do not want to have any internal references to the graph data structure being available
to non-class members. Thus, we elected to return a clone of the adjacency list for our
neighbors method. We did not want to keep redundant data so the order of our graph
is simply the size of the _adj vector. We let the Vector class over allocate space, if
needed, so we do not need a _space variable either.
// default output readable by constructor
//
public String toString() { return toStringAdjLists(); }
} // end class GraphAdjLists
Again, we have the default output format for this class be compatible with the constructor
BufferedReader.
B.3 Standardized Java graph class
We now have two implementations of a graph class as specified by our abstract class
GraphADT. We want to write algorithms that can handle either format. Since Java
is object-oriented we could have all our algorithms take a GraphADT object and the
run-time dynamic mechanism should ascertain the correct adjacency matrix or adjacency
lists methods. For example, we could write a graph coloring algorithm prototyped as
public int color(GraphADT G) and pass it either a GraphAdjMatrix or a
GraphAdjLists. And it should work fine!
However, being humans and not liking to do so much typing, we decide to use the simpler
(and much more common) name Graph for the graph object in our algorithms. This
246
COMPSCI.220FT
assumes the user has defined a Graph class as follows: 3
// uncomment one of the following lines as the current representation
//
public class Graph extends GraphAdjLists
//public class Graph extends GraphAdjMatrix
{
public Graph() { super(); }
public Graph(BufferedReader in) { super(in); }
public Graph(Graph G) { super(G); }
}
We use the super calls to the base class constructors for a transparent interface. A test
program for our graph class is given below (for Graph being represented in this program
as GraphAdjLists).
public class test {
public static void main(String argv[])
{
Graph G = new Graph();
G.addVertices(5); G.addArc(0,2); G.addArc(0,3); G.addEdge(1,2);
G.addArc(2,3); G.addArc(2,0); G.addArc(2,4); G.addArc(3,2);
G.addEdge(4,1); G.addArc(4,2);
System.out.println(G);
G.removeArc(2,0); G.removeArc(4,1); G.removeArc(2,3);
System.out.println(G.toStringAdjMatrix());
G.addVertices(2); G.addArc(5,4); G.addArc(5,2); G.addArc(5,6);
G.addArc(2,6); G.addArc(0,6); G.addArc(6,0);
System.out.println(G);
G.removeVertex(4); G.removeEdge(5,0); G.addVertices(1);
G.addEdge(1,6);
System.out.println(G.toStringAdjLists());
}
} // test
3
Alternatively, we could have renamed GraphADT as Graph. However, being somewhat modest, we
elected not to establish this as the “standard” graph class.
COMPSCI.220FT
247
The expected output, using JDK version 1.1.6, is given below. Note that the last version
of the digraph G has a vertex of out-degree zero in the adjacency lists. (To compile our
program we type ‘javac test.java’ and to execute it we type ‘java test’ at our command-line
prompt ‘$’.)
$
$
5
2
2
0
2
1
5
0
0
0
0
0
7
2
2
1
2
2
2
0
7
2
2
1
2
2
javac test.java
java test
3
4
1 3 4
2
0
0
1
0
0
1
1
0
1
1
1
0
0
0
0
0
1
1
0
0
3 6
4
4 6
4 6
3
6
5
5
1
B.4 Graph algorithms
All of our graph algorithms (public static methods) will be placed in a class called:
public class GraphAlgs
{
// search algorithms
//
248
COMPSCI.220FT
static public int BFS( Graph G, int v, int[] LevelOrder )
{ ... }
static public int DFS( Graph G, int v,
int[] PreOrder, int[] PostOrder )
{ ... }
// other algorithms added later
//
// ...
}
// class GraphAlgs
Our breadth-first search algorithm will use a queue of Integers to store the most recently visited vertices and use the head of this queue to dictate how to explore the graph
further.
static public int BFS(Graph G, int v, int[] LevelOrder)
{
int n = G.order();
for( int i = 0; i < n; i++ ) LevelOrder[i] = 0;
Queue toGrow = new Queue();
int cnt = 0;
LevelOrder[v] = ++cnt;
toGrow.enque(new Integer(v));
while (!toGrow.empty())
{
int grow = ((Integer) toGrow.deque()).intValue();
Vector nbrs = G.neighbors(grow);
for( int i = 0; i < nbrs.size(); i++ )
{
int u = ((Integer) nbrs.elementAt(i)).intValue();
if ( LevelOrder[u] == 0 )
// i.e., not visited yet
{
LevelOrder[u] = ++cnt;
toGrow.enque( new Integer(u) );
}
}
}
return cnt;
}
Note that we have to write our own class Queue since Java has only a class Stack in
COMPSCI.220FT
249
the API4 . At the end of the DFS subsection we will show a test program using this BFS
algorithm (as well as the DFS algorithm) on the two graphs of Example 15.
For our DFS algorithm we will write a recursive algorithm to do the backtracking. To
keep both the pre-order and post-order label of the vertices we define a private inner-class
countPair (of class GraphAlgs). The actual recursive search is also done with a private method called doDFS, which will hide the existence of the local class countPair.
static private class countPair // private counters for DFS search
{
int cnt1, cnt2;
int inc1() { return ++cnt1; } // increment and return counters
int inc2() { return ++cnt2; }
};
static private
void doDFS(Graph G, int v, int[] PreOrder,
int[] PostOrder, countPair cnt)
{
PreOrder[v] = cnt.inc1();
Vector nbrs = G.neighbors(v);
for( int i = 0; i < nbrs.size(); i++ )
{
int u = ((Integer) nbrs.elementAt(i)).intValue();
if ( PreOrder[u] == 0 )
// Have we not seen vertex u before?
{
doDFS(G, u, PreOrder, PostOrder, cnt);
}
}
PostOrder[v] = cnt.inc2();
return;
}
static public int DFS(Graph G, int v,
int[] PreOrder, int[] PostOrder)
{
int n = G.order();
for( int i = 0; i < n; i++ ) PreOrder[i] = PostOrder[i] = 0;
// returns number of nodes reached
//
countPair cnt = new countPair();
doDFS(G, v, PreOrder, PostOrder, cnt);
4
In Java 2 (JDK 1.2) one would simply use (i.e. extend) the java.util.LinkedList class.
250
COMPSCI.220FT
return PostOrder[v];
}
Note that the user has to preallocate the arrays PreOrder and PostOrder before calling the DFS algorithm. Recall that array parameter variables are references to the storage
allocated in the calling methods. If we allocated new storage in the method DFS then the
calling methods would not be able to access the results.
We give a test search program below that calls both the BFS and DFS algorithms on a
stream of graphs (either from the keyboard or redirected from a file).
import java.io.*;
public class search {
public static void main(String argv[])
{
try {
// BufferedReader input = new BufferedReader(new FileReader(argv[0]));
BufferedReader input =
new BufferedReader(new InputStreamReader(System.in));
while(true)
{
Graph G = new Graph(input);
int n=G.order(); if ( n == 0 ) break;
System.out.print(G.toStringAdjLists());
int preOrder[] = new int[n];
int postOrder[] = new int[n];
GraphAlgs.BFS(G,0,preOrder);
System.out.print("BFS (levelorder): ");
for( int i=0; i<n; i++) System.out.print(preOrder[i] + " ");
System.out.print("\n");
GraphAlgs.DFS(G,0,preOrder,postOrder);
System.out.print("DFS (preorder):
");
for( int i=0; i<n; i++) System.out.print(preOrder[i] + " ");
System.out.print("\n");
System.out.print("DFS (postorder): ");
for( int i=0; i<n; i++) System.out.print(postOrder[i] + " ");
COMPSCI.220FT
251
System.out.print("\n");
}
} catch ( Exception e ) { System.err.println("Exception: "+e); }
} // main
} // class search
We run this program on the two graphs of Example 15 below:
$
9
1
0
0
1
1
3
3
4
2
java search <ex2.lists
2
2
1
5
2
3 4
4 8
6
7 8
8
4 7
BFS (levelorder): 1 2 3 4 5 7 8 9 6
DFS (preorder):
1 2 3 7 4 8 9 5 6
DFS (postorder): 9 8 4 7 3 5 6 2 1
7
1
6
0
1
5
0
2 3
4
5 6
2 4 6
BFS (levelorder): 1 2 3 4 6 7 5
DFS (preorder):
1 2 4 7 5 6 3
DFS (postorder): 7 2 5 6 4 3 1
Each graph is echoed to the screen then the pre-order labellings of the BFS and DFS
searches are printed, followed by the post-order labels of the DFS search. The reader can
verify that these labels are consistent with Examples 16 and 18
We can use the following class for converting a digraph to a graph.
252
COMPSCI.220FT
public class uGraph extends Graph
{
public uGraph() { super(); }
public uGraph(uGraph G) { super(G); }
public uGraph(Graph G)
{
super(G);
int n = order();
// create underlying graph by adding symmetric edges
//
for( int i=0; i<n-1; i++)
for( int j=i+1; j<n; j++)
{
if ( isArc(i,j) ) super.addArc(j,i);
else
if ( isArc(j,i) ) super.addArc(i,j);
}
}
// public uGraph(BufferedReader) -- use base class directly.
// redefine size for undirected graph
//
public int size() { return super.size()/2; }
// ... also directed edge methods need fixing
// (would like to privatize them but Java doesn’t allow it)
//
public void addArc(int i, int j) { super.addEdge(i,j); }
public void removeArc(int i, int j) { super.removeEdge(i,j); }
//public boolean isArc(int i, int j) { return super.isArc(i,j); }
}
The constructor from a graph (digraph) makes sure that arcs are provided in both directions. We also redefined some of the edge related methods (e.g., size() returns number of
edges, not the number of arcs).
We now present some connectivity algorithms.
static public boolean isConnected(uGraph G)
{
int[] tmp = new int[G.order()];
COMPSCI.220FT
return BFS(G,0,tmp) == G.order();
}
static public boolean isConnected(Graph G) // for underlying graph
{
uGraph G2 = new uGraph(G);
return isConnected(G2);
}
static public boolean isStronglyConnected(Graph G)
{
int n = G.order();
int[] pre = new int[n];
int[] post = new int[n];
if (DFS(G,0,pre,post) < n) return false;
Graph R = new Graph();
// copy of G with all arcs reversed
R.addVertices(n);
//
for( int i = 0; i < n; i++ )
{
Vector nbrs = G.neighbors(i);
for( int k = 0; k < nbrs.size(); k++ )
{
int u = ((Integer) nbrs.elementAt(k)).intValue();
R.addArc(u,i);
}
}
return (DFS(R,0,pre,post) == n);
}
Methods for checking acyclicity now follow.
static public boolean isAcyclic(uGraph G)
{
int n=G.order();
int m=G.size();
// for O(n) running time [adjacency lists]
if (m >= n) return false;
int[] PreOrder = new int[n];
int[] PostOrder = new int[n];
countPair cnt = new countPair();
int components = 0;
253
254
COMPSCI.220FT
//
for( int i = 0; i < n; i++ )
{
if (PreOrder[i] > 0) continue;
// else try next component with root i
//
doDFS(G, i, PreOrder, PostOrder, cnt);
components++;
}
return n == m + components;
}
static public boolean isAcyclic(Graph G) // no cycles
// (of length > 1)?
{
int n=G.order();
int[] PreOrder = new int[n];
int[] PostOrder = new int[n];
boolean[] span = new boolean[n];
for (int i=0; i<n; i++) // check if any vertex is on a cycle
{
if ( span[i] ) continue; // try next component
int cnt = DFS(G, i, PreOrder, PostOrder);
for( int j = 0; j < n; j++)
{
if ( PreOrder[j] > 0 ) span[j] = true;
Vector nbrs = G.neighbors(j);
for( int k = 0; k < nbrs.size(); k++ )
{
int u = ((Integer) nbrs.elementAt(k)).intValue();
if ( PostOrder[u] > PostOrder[j] )
return false;
// Bingo!
// note: PostOrder[u] > 0 since u is reachable from j
}
}
if (cnt == n) break;
}
return true;
// all vertices spanned
}
Acyclic digraphs do not have cycles of length two so this algorithm will return false for
any undirected graph with at least one edge.
COMPSCI.220FT
255
We used a boolean array span to make sure that we have investigated all vertices of the
graph. The i loop guarantees that each vertex is in some DFS tree.
In the following Java code we decided to illustrate a slightly different programming style.
Instead of calling the BFS method directly (like we did above for our isAcyclic algorithm with DFS) we modify the BFS code as needed. We also use the Java BitSet class
instead of a boolean array (span) and use Vector and Enumeration classes instead
of our Queue class.
static public int girth(Graph Gin) // returns minimum girth >= 3
{
uGraph G = new uGraph(Gin);
// compute undirected girth
int n = G.order();
int best = n + 1;
// girth n+1 if no cycles found
for ( int i = 0; i < n - 2; i++ )
{
// look for a cycle from all but last two
BitSet span = new BitSet(n);
span.set(i);
int depth = 1;
// do a BFS search keeping track of depth
Vector distList = new Vector();
distList.addElement( new Integer(i) );
while (depth*2 <= best && best > 3)
{
Vector nextList = new Vector();
for( Enumeration e = distList.elements();
e.hasMoreElements();
)
{
Vector nbrs =
G.neighbors(((Integer) e.nextElement()).intValue());
for( int k = 0; k < nbrs.size(); k++ )
{
int u = ((Integer) nbrs.elementAt(k)).intValue();
if ( !span.get(u) )
{
span.set(u);
nextList.addElement(new Integer(u));
}
else
// we have found some walk/cycle
{
// is there a cross edge at this level
//
256
COMPSCI.220FT
if ( distList.contains(new Integer(u)) )
{
best = depth * 2 - 1; break;
}
// else even length cycle (as an upper bound)
//
if ( nextList.contains(new Integer(u)) )
{
best = depth * 2;
}
}
}
}
// for vertices at current depth
distList = nextList;
// next try set of vertices
// further away
depth++;
}
}
return best;
}
Note that we return jV j + 1 if we do not find any cycles. Since we are originating a BFS
from all but two of the vertices we do not have to separately worry about ensuring all
components have been checked. The line
while (depth*2 <= best && best > 3)
indicates that we stop searching if the depth of the current BFS tree is greater than an
already found cycle. We also break out of a grow loop above if we find a small cycle (a
cross edge on the current level).
Next is a method to check whether a graph is bipartite.
static public boolean isBipartite( Graph Gin )
{
uGraph G = new uGraph(Gin);
// color underlying graph
int n = G.order();
int color[] = new int[n];
// will toggle between 1 and 2
for( int v = 0; v<n; v++ )
{
if ( color[v] > 0 ) continue;
color[v] = 1;
// start at first vertex
Queue toGrow = new Queue();
toGrow.enque(new Integer(v));
// use BFS queue search
COMPSCI.220FT
257
while ( !toGrow.empty() )
{
int grow = ((Integer) toGrow.deque()).intValue();
Vector nbrs = G.neighbors(grow);
for( int i = 0; i < nbrs.size(); i++ )
{
int u = ((Integer) nbrs.elementAt(i)).intValue();
if ( color[u] == 0 )
// not colored yet
{
color[u] = 3 - color[grow];
// set to other color
toGrow.enque(new Integer(u));
}
else
// check for different color
{
if ( color[u] == color[grow] ) return false;
}
}
} // more nodes in this component
} // while all components have been checked
return true;
}
The only tricky part of the above algorithm is our way of switching between color 1 and
2, (3 minus “previous color”). We use the array color to indicate when all vertices (all
components) have been colored.
Below we give a test program for the last three algorithms. This time we read in a file
of graphs using the first command-line argument as the filename. After printing out an
adjacency lists representation of each graph we decide whether it is acyclic, what the
underlying girth is, and determine whether it is bipartite (2-colorable). The first graph of
order 9 is a tree (but cyclic as viewed as a digraph). Having girth undefined (denoted by
10) tells us that it is a tree. The second graph is a DAG but does have an underlying 3
cycle (and thus is not bipartite). Our algorithms did correctly find the 4 cycle of the third
graph as its girth and concluded that it is 2-colorable (has only even-length cycles).
import java.io.*;
public class cycles {
public static void main(String argv[])
258
COMPSCI.220FT
{
try {
BufferedReader input = new BufferedReader(new FileReader(argv[0]));
while(true)
{
Graph G = new Graph(input);
int n = G.order(); if ( n == 0 ) break;
System.out.print(G.toStringAdjLists());
if ( GraphAlgs.isAcyclic(G) )
{
System.out.print("Graph is acyclic, ");
}
else
{
System.out.print("Graph is not acyclic, ");
}
System.out.print("has underlying girth " + GraphAlgs.girth(G));
if ( GraphAlgs.isBipartite(G) )
{
System.out.println(", and is 2-colourable.");
}
else
{
System.out.println(", and is not 2-colourable.");
}
}
} catch ( Exception e ) { System.err.println("Exception: "+e); }
} // main
} // class cycles
A sample run on the three graphs of Example 27 is given below. (The $ line indicates
what the author typed at his unix command prompt.)
$ java cycles ex3.lists
9
2
3
0 3
1 2 4 5
3
COMPSCI.220FT
3 6 7 8
5
5
5
Graph is not acyclic, has underlying girth 10, and is 2-colorable.
7
1 2
4 5
3
6
6
3 6
Graph is acyclic, has underlying girth 3, and is not 2-colorable.
9
1
0
0
2
1
1
3
4
6
2
4
3
6
6
2
4
5
7
5
5
7
7
8
8
Graph is not acyclic, has underlying girth 4, and is 2-colorable.
Next, some methods for topological sorting.
static public int[] topSort1(Graph G, int firstV)
{
int n = G.order();
int[] PreOrder = new int[n];
int[] PostOrder = new int[n];
int[] sort = new int[n];
int cnt = DFS(G, firstV, PreOrder, PostOrder);
for( int i = 0; i < n; i++)
sort[i] = cnt - PostOrder[i] + 1;
return sort;
}
static public int[] topSort2(Graph G)
259
260
COMPSCI.220FT
{
int n = G.order();
int[] sort = new int[n];
int[] inDeg = new int[n];
for( int i = 0; i < n; i++ ) inDeg[i] = G.inDegree(i);
int cnt = 0;
boolean progress = true;
//
while (progress)
{
progress = false;
for (int v=0; v<n; v++)
{
if ( inDeg[v] == 0 )
{
sort[v] = ++cnt;
progress = true;
inDeg[v] = -1;
Vector nbrs = G.neighbors(v);
for( int k = 0; k < nbrs.size(); k++ )
{
int u = ((Integer) nbrs.elementAt(k)).intValue();
inDeg[u] = inDeg[u] - 1;
}
}
} // for v
} // while nodes exist with inDegree == 0.
return sort;
}
Below we present a test program for computing one topological order for each input graph
of a stream of DAGs. We assume that vertex 0 is the first vertex (which is required for
method topSort1).
import java.io.*;
public class topsort {
private static void printSort(String label, int[] sort)
{
int n = sort.length;
int actual[] = new int[n];
COMPSCI.220FT
261
System.out.print(label);
for( int i = 0; i < n; i++ ) actual[sort[i] - 1] = i;
for( int i = 0; i < n; i++ )
System.out.print(actual[i] + " ");
System.out.println();
}
public static void main(String argv[])
{
try {
BufferedReader input =
new BufferedReader(new InputStreamReader(System.in))
while(true)
{
Graph G = new Graph(input);
int n = G.order(); if ( n == 0 ) break;
System.out.print( G.toStringAdjLists() );
printSort( "sort1: ", GraphAlgs.topSort1(G,0) );
printSort( "sort2: ", GraphAlgs.topSort2(G) );
}
} catch ( Exception e ) {
System.err.println("Exception: "+e);
}
} // main
} // class topsort
Notice how we wrote an auxiliary method printSort to display the vertex indices in
the given topological order. The output of this program, when run on the three DAGs of
Example 31, is given next.
$
5
1
3
3
java topsort <ex4.lists
2
4
4
sort1: 0 2 1 4 3
sort2: 0 1 2 3 4
6
1
2 3
4 5
262
COMPSCI.220FT
sort1: 0 1 3 2 5 4
sort2: 0 1 2 3 4 5
7
1 2 5
2 3 4 5
3 4
6
6
3 6
sort1: 0 1 5 2 4 3 6
sort2: 0 1 2 4 5 3 6
Both algorithms do produce valid topological orders for their input. Notice how each
algorithm gives a different (and correct) result.
The following method maxDistance returns the maximum distance of any of the vertices or jV j if not all vertices are reachable, along with setting an integer array dist with
the corresponding shortest distances.
static public int maxDistance( Graph G, int v, int dist[] )
{
int n = G.order();
for (int i=0; i<n; i++) dist[i]=n;
dist[v]=0;
int depth = 1;
int cnt = 1;
// set to maximum distance
// next distance in BFS
// how many vertices reachable
Vector distList = new Vector();
distList.addElement( new Integer(v) );
while( distList.size() > 0 )
{
Vector nextList = new Vector();
for( Enumeration e = distList.elements();
e.hasMoreElements();
)
{
Vector nbrs =
G.neighbors(((Integer) e.nextElement()).intValue());
for( int k = 0; k < nbrs.size(); k++ )
{
int u = ((Integer) nbrs.elementAt(k)).intValue();
COMPSCI.220FT
263
if (dist[u] == n)
// first time reachable?
{
cnt++;
dist[u] = depth;
nextList.addElement( new Integer(u) );
}
}
}
distList = nextList;
// next try set of vertices
// further away
depth++;
}
return cnt == n ? depth-2 : n;
}
In the above method we did a breadth-first search and kept a counter cnt to indicate if
we have reached all of the vertices.
We can now calculate the diameter of a graph (or digraph) by calling the maxDistance
method from each node.
static public int diameter( Graph G )
{
int n = G.order();
int max = 0;
int [] dist = new int[n];
for( int v = 0; v < n; v++ )
{
int d = maxDistance( G, v, dist );
max = d > max ? d : max;
}
return max;
}
public static int[][] distanceMatrix( Graph G )
{
int n = G.order();
int D[][] = new int[n][n];
for( int v = 0; v < n; v++ )
{
maxDistance( G, v, D[v] );
}
return D;
}
Notice how we can pass in a row of the defined distance matrix D to our maxDistance
method, which accepts an integer array for assigning the path distances.