From 51b97f7f4fc16630cb8ee2fb54ba1e59785174e4 Mon Sep 17 00:00:00 2001 From: Connor Newton Date: Tue, 25 Jul 2017 19:04:02 +0100 Subject: [PATCH 1/2] Add Tarjan's strongly connected components algorithm --- src/StronglyConnectedComponents.php | 121 ++++++++++++++++++++++ tests/StronglyConnectedComponentsTest.php | 93 +++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100755 src/StronglyConnectedComponents.php create mode 100644 tests/StronglyConnectedComponentsTest.php 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 From 3f712af35e159104d76a04935e69a3dad4efc07c Mon Sep 17 00:00:00 2001 From: Connor Newton Date: Sun, 7 Oct 2018 18:54:04 +0100 Subject: [PATCH 2/2] Add elementary cycles algorithm --- src/SimpleCycles.php | 106 +++++++++++++++++++++++++++++++++++++ tests/SimpleCyclesTest.php | 90 +++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 src/SimpleCycles.php create mode 100644 tests/SimpleCyclesTest.php diff --git a/src/SimpleCycles.php b/src/SimpleCycles.php new file mode 100644 index 0000000..7935e99 --- /dev/null +++ b/src/SimpleCycles.php @@ -0,0 +1,106 @@ +blockedSet[$v->getId()]); + if (isset($this->blockedMap[$v->getId()])) { + foreach ($this->blockedMap[$v->getId()] as $w) { + if (isset($this->blockedSet[$w->getId()])) { + $this->unblock($w); + } + } + unset($this->blockedMap[$v->getId()]); + } + } + + /** + * Recursive node visit procedure + * @param Vertex $r Root node + * @param Vertex $v Visited node + * @return bool Found cycle + */ + protected function visit(Vertex $r, Vertex $v) + { + array_push($this->stack, $v); + $this->blockedSet[$v->getId()] = $v; + $found = false; + // Examine adjacent nodes + foreach ($v->getVerticesEdgeTo() as $w) { + if ($w->getId() === $r->getId()) { + // Adjacent node == start node .'. found a cycle + $found = true; + // Cycle is whatever is on the stack + $this->cycles[] = new Vertices($this->stack); + } else if (!isset($this->blockedSet[$w->getId()])) { + // Only visit adjacent node if not blocked + $found = $this->visit($r, $w) || $found; + } + } + if ($found) { + // If cycle found, block node and all nodes mapped to it + $this->unblock($v); + } else { + // Otherwise add node to all adjacent nodes' blocked map + foreach ($v->getVerticesEdgeTo() as $w) { + if (!isset($this->blockedMap[$w->getId()])) { + $this->blockedMap[$w->getId()] = array(); + } + $this->blockedMap[$w->getId()][$v->getId()] = $v; + } + } + array_pop($this->stack); + return $found; + } + + /** + * @param Graph $g + */ + protected function findCycles(Graph $g) + { + $this->blockedSet = array(); + $this->blockedMap = array(); + $this->stack = array(); + foreach ($g->getVertices() as $v) { + $this->visit($v, $v); + $v->destroy(); + } + } + + /** + * @param Graph $graph + * @return array|Vertices[] + */ + public function getSimpleCycles(Graph $graph) + { + $this->cycles = array(); + $components = new StronglyConnectedComponents(); + foreach ($components->stronglyConnectedGraph($graph) as $component) { + $this->findCycles($component); + } + return $this->cycles; + } +} \ No newline at end of file diff --git a/tests/SimpleCyclesTest.php b/tests/SimpleCyclesTest.php new file mode 100644 index 0000000..3191a91 --- /dev/null +++ b/tests/SimpleCyclesTest.php @@ -0,0 +1,90 @@ + array( + "edges" => array( + array(8, 9), + array(9, 8), + ), + "cycles" => array( + array(8, 9), + ), + ), + "strongly connected component (isolated)" => array( + "edges" => array( + array(1, 2), + array(1, 5), + array(2, 3), + array(3, 1), + array(3, 2), + array(3, 4), + array(3, 6), + array(4, 5), + array(5, 2), + array(6, 4), + ), + "cycles" => array( + array(1, 2, 3), + array(1, 5, 2, 3), + array(2, 3), + array(2, 3, 4, 5), + array(2, 3, 6, 4, 5), + ), + ), + "strongly connected component (connected)" => array( + "edges" => array( + array(1, 2), + array(1, 5), + array(1, 8), + array(2, 3), + array(2, 7), + array(2, 9), + array(3, 1), + array(3, 2), + array(3, 4), + array(3, 6), + array(4, 5), + array(5, 2), + array(6, 4), + array(8, 9), + array(9, 8), + ), + "cycles" => array( + array(8, 9), + array(1, 2, 3), + array(1, 5, 2, 3), + array(2, 3), + array(2, 3, 4, 5), + array(2, 3, 6, 4, 5), + ), + ), + ); + } + + + + /** + * @dataProvider providerGetSimpleCycles + */ + public function testGetSimpleCycles(array $edges, array $cycles) + { + $graph = new Graph(); + foreach ($edges as $edge) { + $graph->createVertex($edge[0], true)->createEdgeTo($graph->createVertex($edge[1], true)); + } + $alg = new SimpleCycles(); + $actual = array(); + foreach ($alg->getSimpleCycles($graph) as $cycle) { + $actual[] = $cycle->getIds(); + } + $this->assertSame($cycles, $actual); + } +} \ No newline at end of file