tom

Added Tarjan SCC computation algorithm and associated tests.

package org.onlab.graph;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Tarjan algorithm for searching a graph and producing results describing
* the graph SCC (strongly-connected components).
*/
public class TarjanGraphSearch<V extends Vertex, E extends Edge<V>>
implements GraphSearch<V, E> {
/**
* {@inheritDoc}
* <p/>
* This implementation produces results augmented with information on
* SCCs within the graph.
* <p/>
* To prevent traversal of an edge, the {@link EdgeWeight#weight} should
* return a negative value as an edge weight.
*/
@Override
public SCCResult<V, E> search(Graph<V, E> graph, EdgeWeight<V, E> weight) {
SCCResult<V, E> result = new SCCResult<>(graph);
for (V vertex : graph.getVertexes()) {
VertexData data = result.data(vertex);
if (data == null) {
connect(graph, vertex, weight, result);
}
}
return result.build();
}
/**
* Scans the specified graph, using recursion, and produces SCC results.
*
* @param graph graph to search
* @param vertex current vertex to scan and connect
* @param weight optional edge weight
* @param result graph search result
* @return augmentation vertexData for the current vertex
*/
private VertexData<V> connect(Graph<V, E> graph, V vertex,
EdgeWeight<V, E> weight,
SCCResult<V, E> result) {
VertexData<V> data = result.addData(vertex);
// Scan through all egress edges of the current vertex.
for (E edge : graph.getEdgesFrom(vertex)) {
V nextVertex = edge.dst();
// If edge weight is negative, skip it.
if (weight != null && weight.weight(edge) < 0) {
continue;
}
// Attempt to get the augmentation vertexData for the next vertex.
VertexData<V> nextData = result.data(nextVertex);
if (nextData == null) {
// Next vertex has not been visited yet, so do this now.
nextData = connect(graph, nextVertex, weight, result);
data.lowLink = Math.min(data.lowLink, nextData.lowLink);
} else if (result.visited(nextData)) {
// Next vertex has been visited, which means it is in the
// same cluster as the current vertex.
data.lowLink = Math.min(data.lowLink, nextData.index);
}
}
if (data.lowLink == data.index) {
result.addCluster(data);
}
return data;
}
/**
* Graph search result augmented with SCC vertexData.
*/
public static final class SCCResult<V extends Vertex, E extends Edge<V>>
implements Result {
private final Graph<V, E> graph;
private List<Set<V>> clusterVertexes = new ArrayList<>();
private List<Set<E>> clusterEdges = new ArrayList<>();
private int index = 0;
private final Map<V, VertexData<V>> vertexData = new HashMap<>();
private final List<VertexData<V>> visited = new ArrayList<>();
private SCCResult(Graph<V, E> graph) {
this.graph = graph;
}
/**
* Returns the number of SCC clusters in the graph.
*
* @return number of clusters
*/
public int clusterCount() {
return clusterEdges.size();
}
/**
* Returns the list of strongly connected vertex clusters.
*
* @return list of strongly connected vertex sets
*/
public List<Set<V>> clusterVertexes() {
return clusterVertexes;
}
/**
* Returns the list of edges linking strongly connected vertex clusters.
*
* @return list of strongly connected edge sets
*/
public List<Set<E>> clusterEdges() {
return clusterEdges;
}
// Gets the augmentation vertexData for the specified vertex
private VertexData<V> data(V vertex) {
return vertexData.get(vertex);
}
// Adds augmentation vertexData for the specified vertex
private VertexData<V> addData(V vertex) {
VertexData<V> d = new VertexData<>(vertex, index);
vertexData.put(vertex, d);
visited.add(0, d);
index++;
return d;
}
// Indicates whether the given vertex has been visited
private boolean visited(VertexData data) {
return visited.contains(data);
}
// Adds a new cluster for the specified vertex
private void addCluster(VertexData data) {
Set<V> vertexes = findClusterVertices(data);
clusterVertexes.add(vertexes);
clusterEdges.add(findClusterEdges(vertexes));
}
private Set<V> findClusterVertices(VertexData data) {
VertexData<V> nextVertexData;
Set<V> vertexes = new HashSet<>();
do {
nextVertexData = visited.remove(0);
vertexes.add(nextVertexData.vertex);
} while (data != nextVertexData);
return Collections.unmodifiableSet(vertexes);
}
private Set<E> findClusterEdges(Set<V> vertexes) {
Set<E> edges = new HashSet<>();
for (V vertex : vertexes) {
for (E edge : graph.getEdgesFrom(vertex)) {
if (vertexes.contains((edge.dst()))) {
edges.add(edge);
}
}
}
return Collections.unmodifiableSet(edges);
}
public SCCResult<V, E> build() {
clusterVertexes = Collections.unmodifiableList(clusterVertexes);
clusterEdges = Collections.unmodifiableList(clusterEdges);
return this;
}
}
// Augments the vertex to assist in determining SCC clusters.
private static final class VertexData<V extends Vertex> {
final V vertex;
int index;
int lowLink;
private VertexData(V vertex, int index) {
this.vertex = vertex;
this.index = index;
this.lowLink = index;
}
}
}
......@@ -31,17 +31,17 @@ public class BellmanFordGraphSearchTest extends BreadthFirstSearchTest {
@Test
public void searchGraphWithNegativeCycles() {
Set<TestVertex> vertexes = new HashSet<>(vertices());
Set<TestVertex> vertexes = new HashSet<>(vertexes());
vertexes.add(Z);
Set<TestEdge> edges = new HashSet<>(edges());
edges.add(new TestEdge(G, Z, 1.0));
edges.add(new TestEdge(Z, G, -2.0));
g = new AdjacencyListsGraph<>(vertexes, edges);
graph = new AdjacencyListsGraph<>(vertexes, edges);
GraphPathSearch<TestVertex, TestEdge> search = graphSearch();
Set<Path<TestVertex, TestEdge>> paths = search.search(g, A, H, weight).paths();
Set<Path<TestVertex, TestEdge>> paths = search.search(graph, A, H, weight).paths();
assertEquals("incorrect paths count", 1, paths.size());
Path p = paths.iterator().next();
......@@ -50,10 +50,10 @@ public class BellmanFordGraphSearchTest extends BreadthFirstSearchTest {
assertEquals("incorrect path length", 5, p.edges().size());
assertEquals("incorrect path cost", 5.0, p.cost(), 0.1);
paths = search.search(g, A, G, weight).paths();
paths = search.search(graph, A, G, weight).paths();
assertEquals("incorrect paths count", 0, paths.size());
paths = search.search(g, A, null, weight).paths();
paths = search.search(graph, A, null, weight).paths();
printPaths(paths);
assertEquals("incorrect paths count", 6, paths.size());
}
......
......@@ -29,10 +29,10 @@ public class BreadthFirstSearchTest extends AbstractGraphPathSearchTest {
// Executes the default test
protected void executeDefaultTest(int pathCount, int pathLength, double pathCost) {
g = new AdjacencyListsGraph<>(vertices(), edges());
graph = new AdjacencyListsGraph<>(vertexes(), edges());
GraphPathSearch<TestVertex, TestEdge> search = graphSearch();
Set<Path<TestVertex, TestEdge>> paths = search.search(g, A, H, weight).paths();
Set<Path<TestVertex, TestEdge>> paths = search.search(graph, A, H, weight).paths();
assertEquals("incorrect paths count", 1, paths.size());
Path p = paths.iterator().next();
......@@ -41,7 +41,7 @@ public class BreadthFirstSearchTest extends AbstractGraphPathSearchTest {
assertEquals("incorrect path length", pathLength, p.edges().size());
assertEquals("incorrect path cost", pathCost, p.cost(), 0.1);
paths = search.search(g, A, null, weight).paths();
paths = search.search(graph, A, null, weight).paths();
printPaths(paths);
assertEquals("incorrect paths count", pathCount, paths.size());
}
......
......@@ -33,11 +33,11 @@ public class DepthFirstSearchTest extends AbstractGraphPathSearchTest {
protected void executeDefaultTest(int minLength, int maxLength,
double minCost, double maxCost) {
g = new AdjacencyListsGraph<>(vertices(), edges());
graph = new AdjacencyListsGraph<>(vertexes(), edges());
DepthFirstSearch<TestVertex, TestEdge> search = graphSearch();
DepthFirstSearch<TestVertex, TestEdge>.SpanningTreeResult result =
search.search(g, A, H, weight);
search.search(graph, A, H, weight);
Set<Path<TestVertex, TestEdge>> paths = result.paths();
assertEquals("incorrect path count", 1, paths.size());
......@@ -57,12 +57,12 @@ public class DepthFirstSearchTest extends AbstractGraphPathSearchTest {
}
public void executeBroadSearch() {
g = new AdjacencyListsGraph<>(vertices(), edges());
graph = new AdjacencyListsGraph<>(vertexes(), edges());
DepthFirstSearch<TestVertex, TestEdge> search = graphSearch();
// Perform narrow path search to a specific destination.
DepthFirstSearch<TestVertex, TestEdge>.SpanningTreeResult result =
search.search(g, A, null, weight);
search.search(graph, A, null, weight);
assertEquals("incorrect paths count", 7, result.paths().size());
int[] types = new int[]{0, 0, 0, 0};
......
......@@ -32,22 +32,22 @@ public class DijkstraGraphSearchTest extends BreadthFirstSearchTest {
@Test
public void noPath() {
g = new AdjacencyListsGraph<>(of(A, B, C, D),
of(new TestEdge(A, B, 1),
new TestEdge(B, A, 1),
new TestEdge(C, D, 1),
new TestEdge(D, C, 1)));
graph = new AdjacencyListsGraph<>(of(A, B, C, D),
of(new TestEdge(A, B, 1),
new TestEdge(B, A, 1),
new TestEdge(C, D, 1),
new TestEdge(D, C, 1)));
GraphPathSearch<TestVertex, TestEdge> gs = graphSearch();
Set<Path<TestVertex, TestEdge>> paths = gs.search(g, A, B, weight).paths();
Set<Path<TestVertex, TestEdge>> paths = gs.search(graph, A, B, weight).paths();
printPaths(paths);
assertEquals("incorrect paths count", 1, paths.size());
assertEquals("incorrect path cost", 1.0, paths.iterator().next().cost(), 0.1);
paths = gs.search(g, A, D, weight).paths();
paths = gs.search(graph, A, D, weight).paths();
printPaths(paths);
assertEquals("incorrect paths count", 0, paths.size());
paths = gs.search(g, A, null, weight).paths();
paths = gs.search(graph, A, null, weight).paths();
printPaths(paths);
assertEquals("incorrect paths count", 1, paths.size());
assertEquals("incorrect path cost", 1.0, paths.iterator().next().cost(), 0.1);
......@@ -55,40 +55,40 @@ public class DijkstraGraphSearchTest extends BreadthFirstSearchTest {
@Test
public void simpleMultiplePath() {
g = new AdjacencyListsGraph<>(of(A, B, C, D),
of(new TestEdge(A, B, 1),
new TestEdge(A, C, 1),
new TestEdge(B, D, 1),
new TestEdge(C, D, 1)));
executeSearch(graphSearch(), g, A, D, weight, 2, 2.0);
graph = new AdjacencyListsGraph<>(of(A, B, C, D),
of(new TestEdge(A, B, 1),
new TestEdge(A, C, 1),
new TestEdge(B, D, 1),
new TestEdge(C, D, 1)));
executeSearch(graphSearch(), graph, A, D, weight, 2, 2.0);
}
@Test
public void denseMultiplePath() {
g = new AdjacencyListsGraph<>(of(A, B, C, D, E, F, G),
of(new TestEdge(A, B, 1),
new TestEdge(A, C, 1),
new TestEdge(B, D, 1),
new TestEdge(C, D, 1),
new TestEdge(D, E, 1),
new TestEdge(D, F, 1),
new TestEdge(E, G, 1),
new TestEdge(F, G, 1),
new TestEdge(A, G, 4)));
executeSearch(graphSearch(), g, A, G, weight, 5, 4.0);
graph = new AdjacencyListsGraph<>(of(A, B, C, D, E, F, G),
of(new TestEdge(A, B, 1),
new TestEdge(A, C, 1),
new TestEdge(B, D, 1),
new TestEdge(C, D, 1),
new TestEdge(D, E, 1),
new TestEdge(D, F, 1),
new TestEdge(E, G, 1),
new TestEdge(F, G, 1),
new TestEdge(A, G, 4)));
executeSearch(graphSearch(), graph, A, G, weight, 5, 4.0);
}
@Test
public void dualEdgeMultiplePath() {
g = new AdjacencyListsGraph<>(of(A, B, C, D, E, F, G, H),
of(new TestEdge(A, B, 1), new TestEdge(A, C, 3),
new TestEdge(B, D, 2), new TestEdge(B, C, 1),
new TestEdge(B, E, 4), new TestEdge(C, E, 1),
new TestEdge(D, H, 5), new TestEdge(D, E, 1),
new TestEdge(E, F, 1), new TestEdge(F, D, 1),
new TestEdge(F, G, 1), new TestEdge(F, H, 1),
new TestEdge(A, E, 3), new TestEdge(B, D, 1)));
executeSearch(graphSearch(), g, A, E, weight, 3, 3.0);
graph = new AdjacencyListsGraph<>(of(A, B, C, D, E, F, G, H),
of(new TestEdge(A, B, 1), new TestEdge(A, C, 3),
new TestEdge(B, D, 2), new TestEdge(B, C, 1),
new TestEdge(B, E, 4), new TestEdge(C, E, 1),
new TestEdge(D, H, 5), new TestEdge(D, E, 1),
new TestEdge(E, F, 1), new TestEdge(F, D, 1),
new TestEdge(F, G, 1), new TestEdge(F, H, 1),
new TestEdge(A, E, 3), new TestEdge(B, D, 1)));
executeSearch(graphSearch(), graph, A, E, weight, 3, 3.0);
}
}
......
......@@ -19,7 +19,7 @@ public class GraphTest {
static final TestVertex H = new TestVertex("H");
static final TestVertex Z = new TestVertex("Z");
protected Graph<TestVertex, TestEdge> g;
protected Graph<TestVertex, TestEdge> graph;
protected EdgeWeight<TestVertex, TestEdge> weight =
new EdgeWeight<TestVertex, TestEdge>() {
......@@ -35,7 +35,7 @@ public class GraphTest {
}
}
protected Set<TestVertex> vertices() {
protected Set<TestVertex> vertexes() {
return of(A, B, C, D, E, F, G, H);
}
......
package org.onlab.graph;
import org.junit.Test;
import static com.google.common.collect.ImmutableSet.of;
import static org.junit.Assert.assertEquals;
import static org.onlab.graph.TarjanGraphSearch.SCCResult;
/**
* Tarjan graph search tests.
*/
public class TarjanGraphSearchTest extends GraphTest {
private void validate(SCCResult<TestVertex, TestEdge> result, int cc) {
System.out.println("Cluster count: " + result.clusterVertexes().size());
System.out.println("Clusters: " + result.clusterVertexes());
assertEquals("incorrect cluster count", cc, result.clusterCount());
}
private void validate(SCCResult<TestVertex, TestEdge> result,
int i, int vc, int ec) {
assertEquals("incorrect cluster count", vc, result.clusterVertexes().get(i).size());
assertEquals("incorrect edge count", ec, result.clusterEdges().get(i).size());
}
@Test
public void basic() {
graph = new AdjacencyListsGraph<>(vertexes(), edges());
TarjanGraphSearch<TestVertex, TestEdge> gs = new TarjanGraphSearch<>();
SCCResult<TestVertex, TestEdge> result = gs.search(graph, null);
validate(result, 6);
}
@Test
public void singleCluster() {
graph = new AdjacencyListsGraph<>(vertexes(),
of(new TestEdge(A, B, 1),
new TestEdge(B, C, 1),
new TestEdge(C, D, 1),
new TestEdge(D, E, 1),
new TestEdge(E, F, 1),
new TestEdge(F, G, 1),
new TestEdge(G, H, 1),
new TestEdge(H, A, 1)));
TarjanGraphSearch<TestVertex, TestEdge> gs = new TarjanGraphSearch<>();
SCCResult<TestVertex, TestEdge> result = gs.search(graph, null);
validate(result, 1);
validate(result, 0, 8, 8);
}
@Test
public void twoUnconnectedCluster() {
graph = new AdjacencyListsGraph<>(vertexes(),
of(new TestEdge(A, B, 1),
new TestEdge(B, C, 1),
new TestEdge(C, D, 1),
new TestEdge(D, A, 1),
new TestEdge(E, F, 1),
new TestEdge(F, G, 1),
new TestEdge(G, H, 1),
new TestEdge(H, E, 1)));
TarjanGraphSearch<TestVertex, TestEdge> gs = new TarjanGraphSearch<>();
SCCResult<TestVertex, TestEdge> result = gs.search(graph, null);
validate(result, 2);
validate(result, 0, 4, 4);
validate(result, 1, 4, 4);
}
@Test
public void twoWeaklyConnectedClusters() {
graph = new AdjacencyListsGraph<>(vertexes(),
of(new TestEdge(A, B, 1),
new TestEdge(B, C, 1),
new TestEdge(C, D, 1),
new TestEdge(D, A, 1),
new TestEdge(E, F, 1),
new TestEdge(F, G, 1),
new TestEdge(G, H, 1),
new TestEdge(H, E, 1),
new TestEdge(B, E, 1)));
TarjanGraphSearch<TestVertex, TestEdge> gs = new TarjanGraphSearch<>();
SCCResult<TestVertex, TestEdge> result = gs.search(graph, null);
validate(result, 2);
validate(result, 0, 4, 4);
validate(result, 1, 4, 4);
}
@Test
public void twoClustersConnectedWithIgnoredEdges() {
graph = new AdjacencyListsGraph<>(vertexes(),
of(new TestEdge(A, B, 1),
new TestEdge(B, C, 1),
new TestEdge(C, D, 1),
new TestEdge(D, A, 1),
new TestEdge(E, F, 1),
new TestEdge(F, G, 1),
new TestEdge(G, H, 1),
new TestEdge(H, E, 1),
new TestEdge(B, E, -1),
new TestEdge(E, B, -1)));
TarjanGraphSearch<TestVertex, TestEdge> gs = new TarjanGraphSearch<>();
SCCResult<TestVertex, TestEdge> result = gs.search(graph, weight);
validate(result, 2);
validate(result, 0, 4, 4);
validate(result, 1, 4, 4);
}
}