diff --git a/utils/misc/src/main/java/org/onlab/graph/TarjanGraphSearch.java b/utils/misc/src/main/java/org/onlab/graph/TarjanGraphSearch.java new file mode 100644 index 0000000000..ebd451fdce --- /dev/null +++ b/utils/misc/src/main/java/org/onlab/graph/TarjanGraphSearch.java @@ -0,0 +1,195 @@ +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> + implements GraphSearch { + + /** + * {@inheritDoc} + *

+ * This implementation produces results augmented with information on + * SCCs within the graph. + *

+ * To prevent traversal of an edge, the {@link EdgeWeight#weight} should + * return a negative value as an edge weight. + */ + @Override + public SCCResult search(Graph graph, EdgeWeight weight) { + SCCResult 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 connect(Graph graph, V vertex, + EdgeWeight weight, + SCCResult result) { + VertexData 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 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> + implements Result { + + private final Graph graph; + private List> clusterVertexes = new ArrayList<>(); + private List> clusterEdges = new ArrayList<>(); + + private int index = 0; + private final Map> vertexData = new HashMap<>(); + private final List> visited = new ArrayList<>(); + + private SCCResult(Graph 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> clusterVertexes() { + return clusterVertexes; + } + + /** + * Returns the list of edges linking strongly connected vertex clusters. + * + * @return list of strongly connected edge sets + */ + public List> clusterEdges() { + return clusterEdges; + } + + // Gets the augmentation vertexData for the specified vertex + private VertexData data(V vertex) { + return vertexData.get(vertex); + } + + // Adds augmentation vertexData for the specified vertex + private VertexData addData(V vertex) { + VertexData 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 vertexes = findClusterVertices(data); + clusterVertexes.add(vertexes); + clusterEdges.add(findClusterEdges(vertexes)); + } + + private Set findClusterVertices(VertexData data) { + VertexData nextVertexData; + Set vertexes = new HashSet<>(); + do { + nextVertexData = visited.remove(0); + vertexes.add(nextVertexData.vertex); + } while (data != nextVertexData); + return Collections.unmodifiableSet(vertexes); + } + + private Set findClusterEdges(Set vertexes) { + Set 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 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 { + final V vertex; + int index; + int lowLink; + + private VertexData(V vertex, int index) { + this.vertex = vertex; + this.index = index; + this.lowLink = index; + } + } + +} diff --git a/utils/misc/src/test/java/org/onlab/graph/BellmanFordGraphSearchTest.java b/utils/misc/src/test/java/org/onlab/graph/BellmanFordGraphSearchTest.java index 4ee9006483..96985b5ba8 100644 --- a/utils/misc/src/test/java/org/onlab/graph/BellmanFordGraphSearchTest.java +++ b/utils/misc/src/test/java/org/onlab/graph/BellmanFordGraphSearchTest.java @@ -31,17 +31,17 @@ public class BellmanFordGraphSearchTest extends BreadthFirstSearchTest { @Test public void searchGraphWithNegativeCycles() { - Set vertexes = new HashSet<>(vertices()); + Set vertexes = new HashSet<>(vertexes()); vertexes.add(Z); Set 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 search = graphSearch(); - Set> paths = search.search(g, A, H, weight).paths(); + Set> 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()); } diff --git a/utils/misc/src/test/java/org/onlab/graph/BreadthFirstSearchTest.java b/utils/misc/src/test/java/org/onlab/graph/BreadthFirstSearchTest.java index f9c752d117..d99e485b23 100644 --- a/utils/misc/src/test/java/org/onlab/graph/BreadthFirstSearchTest.java +++ b/utils/misc/src/test/java/org/onlab/graph/BreadthFirstSearchTest.java @@ -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 search = graphSearch(); - Set> paths = search.search(g, A, H, weight).paths(); + Set> 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()); } diff --git a/utils/misc/src/test/java/org/onlab/graph/DepthFirstSearchTest.java b/utils/misc/src/test/java/org/onlab/graph/DepthFirstSearchTest.java index 8881e84139..944919b0fd 100644 --- a/utils/misc/src/test/java/org/onlab/graph/DepthFirstSearchTest.java +++ b/utils/misc/src/test/java/org/onlab/graph/DepthFirstSearchTest.java @@ -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 search = graphSearch(); DepthFirstSearch.SpanningTreeResult result = - search.search(g, A, H, weight); + search.search(graph, A, H, weight); Set> 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 search = graphSearch(); // Perform narrow path search to a specific destination. DepthFirstSearch.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}; diff --git a/utils/misc/src/test/java/org/onlab/graph/DijkstraGraphSearchTest.java b/utils/misc/src/test/java/org/onlab/graph/DijkstraGraphSearchTest.java index 1731440144..840eddd9e5 100644 --- a/utils/misc/src/test/java/org/onlab/graph/DijkstraGraphSearchTest.java +++ b/utils/misc/src/test/java/org/onlab/graph/DijkstraGraphSearchTest.java @@ -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 gs = graphSearch(); - Set> paths = gs.search(g, A, B, weight).paths(); + Set> 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); } } diff --git a/utils/misc/src/test/java/org/onlab/graph/GraphTest.java b/utils/misc/src/test/java/org/onlab/graph/GraphTest.java index 44b113704b..5c930c4d95 100644 --- a/utils/misc/src/test/java/org/onlab/graph/GraphTest.java +++ b/utils/misc/src/test/java/org/onlab/graph/GraphTest.java @@ -19,7 +19,7 @@ public class GraphTest { static final TestVertex H = new TestVertex("H"); static final TestVertex Z = new TestVertex("Z"); - protected Graph g; + protected Graph graph; protected EdgeWeight weight = new EdgeWeight() { @@ -35,7 +35,7 @@ public class GraphTest { } } - protected Set vertices() { + protected Set vertexes() { return of(A, B, C, D, E, F, G, H); } diff --git a/utils/misc/src/test/java/org/onlab/graph/TarjanGraphSearchTest.java b/utils/misc/src/test/java/org/onlab/graph/TarjanGraphSearchTest.java new file mode 100644 index 0000000000..9bf3acf27d --- /dev/null +++ b/utils/misc/src/test/java/org/onlab/graph/TarjanGraphSearchTest.java @@ -0,0 +1,110 @@ +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 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 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 gs = new TarjanGraphSearch<>(); + SCCResult 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 gs = new TarjanGraphSearch<>(); + SCCResult 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 gs = new TarjanGraphSearch<>(); + SCCResult 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 gs = new TarjanGraphSearch<>(); + SCCResult 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 gs = new TarjanGraphSearch<>(); + SCCResult result = gs.search(graph, weight); + validate(result, 2); + validate(result, 0, 4, 4); + validate(result, 1, 4, 4); + } + +}