diff --git a/src/StronglyConnectedComponents.php b/src/StronglyConnectedComponents.php new file mode 100755 index 0000000..0e67fa1 --- /dev/null +++ b/src/StronglyConnectedComponents.php @@ -0,0 +1,121 @@ +getVerticesEdgeTo()->getVector(); + } + + /** + * Find the strongly connected components of the given graph and return a set of Vertices which + * refer to the original Vertexes + * + * @param Graph $graph + * @return Vertices[] + */ + public function stronglyConnectedVertices(Graph $graph) + { + /** @var SplObjectStorage $preorder int[] */ + $preorder = new SplObjectStorage(); + /** @var SplObjectStorage $lowlink int[] */ + $lowlink = new SplObjectStorage(); + /** @var SplObjectStorage $scc_found bool[] */ + $scc_found = new SplObjectStorage(); + /** @var Vertex[] $scc_queue */ + $scc_queue = array(); + /** @var int $i */ + $i = 0; // preorder counter + /** @var Vertices[] $sccs */ + $sccs = array(); + + foreach ($graph->getVertices() as $source) { + if (!$scc_found->contains($source)) { + $queue = array($source); + while ($queue) { + /** @var Vertex $v */ + $v = end($queue); + if (!$preorder->contains($v)) { + $i++; + $preorder[$v] = $i; + } + /** @var bool $done */ + $done = true; + $v_nbrs = $this->vertexAdjacent($v); + foreach ($v_nbrs as $w) { + if (!$preorder->contains($w)) { + array_push($queue, $w); + $done = false; + break; + } + } + if ($done) { + $lowlink[$v] = $preorder[$v]; + foreach ($v_nbrs as $w) { + if (!$scc_found->contains($w)) { + if ($preorder[$w] > $preorder[$v]) { + $lowlink[$v] = min($lowlink[$v], $lowlink[$w]); + } else { + $lowlink[$v] = min($lowlink[$v], $preorder[$w]); + } + } + } + array_pop($queue); + if ($lowlink[$v] === $preorder[$v]) { + $scc_found->attach($v); + /** @var Vertex[] $scc (Dictionary) */ + $scc = array($v); + while ($scc_queue && $preorder[end($scc_queue)] > $preorder[$v]) { + /** @var Vertex $k */ + $k = array_pop($scc_queue); + $scc_found->attach($k); + array_push($scc, $k); + } + array_push($sccs, new Vertices($scc)); + } else { + array_push($scc_queue, $v); + } + } + } + } + } + + $minSize = $this->minSize; // PHP < 5.4 + return array_filter($sccs, function (Vertices $vertices) use ($minSize) { return $vertices->count() >= $minSize; }); + } + + /** + * Return a set of new Graphs each representing a discovered strongly connected connected component + * + * @param Graph $graph + * @return Graph[] + */ + public function stronglyConnectedGraph(Graph $graph) + { + $sccs = $this->stronglyConnectedVertices($graph); + return array_map(function(Vertices $vertices) use ($graph) { + return $graph->createGraphCloneVertices($vertices); + }, $sccs); + } +} \ No newline at end of file diff --git a/tests/StronglyConnectedComponentsTest.php b/tests/StronglyConnectedComponentsTest.php new file mode 100644 index 0000000..9a9733d --- /dev/null +++ b/tests/StronglyConnectedComponentsTest.php @@ -0,0 +1,93 @@ + array( + 'edges' => array( + array(1,2), + array(2,3), array(2,5), array(2,6), + array(3,4), array(3,7), + array(4,3), array(4,8), + array(5,1), array(5,6), + array(6,7), + array(7,6), + array(8,4), array(8,7), + ), + 'sets' => array( + array(1,2,5), + array(3,4,8), + array(6,7), + ), + ), + /** @link https://en.wikipedia.org/wiki/File:Tarjan%27s_Algorithm_Animation.gif */ + '2' => array( + 'edges' => array( + array(1,2), + array(2,3), + array(3,1), + array(4,2), array(4,3), array(4,5), + array(5,4), array(5,6), + array(6,3), array(6,7), + array(7,6), + array(8,5), array(8,7), + // array(8,8), /* @link https://github.com/clue/graph/issues/154 */ + ), + 'sets' => array( + array(1,2,3), + array(4,5), + array(6,7), + array(8), + ), + ), + ); + } + + /** + * @param Graph $graph + * @param array $edge + * @return Directed + */ + public function addEdgeToGraph(Graph $graph, array $edge) + { + $from = $graph->createVertex($edge[0], true); + $to = $graph->createVertex($edge[1], true); + return $from->createEdgeTo($to); + } + + /** + * @dataProvider providerCyclicDigraphs + * @param array $edges + * @param array $expected + */ + public function testStronglyConnectedSets(array $edges, array $expected) + { + $graph = new Graph(); + + foreach ($edges as $edge) { + $this->addEdgeToGraph($graph, $edge); + } + + $alg = new StronglyConnectedComponents(); + $sets = array_map(function (Vertices $vertices) { + return $vertices->getIds(); + }, $alg->stronglyConnectedVertices($graph)); + + // Assert unordered array equals + $this->assertEquals($expected, $sets, "\$canonicalize = true", 0.0, 2, true); + } +} \ No newline at end of file