diff --git a/src/Tarjan.php b/src/Tarjan.php new file mode 100644 index 0000000..9513ca1 --- /dev/null +++ b/src/Tarjan.php @@ -0,0 +1,105 @@ +hasUndirected()) { + throw new InvalidArgumentException('Graph shall be directed'); + } + + $this->stack = array(); + $this->index = 0; + $this->partition = array(); + $this->indexMap = new \SplObjectStorage(); + $this->lowLinkMap = new \SplObjectStorage(); + + foreach ($graph->getVertices()->getVector() as $vertex) { + if (! isset($this->indexMap[$vertex])) { + $this->recursiveStrongConnect($vertex); + } + } + + return $this->partition; + } + + /** + * Find recursively connected vertices to a vertex and update strongly connected component + * partition with it. + * + * @param Vertex $v + */ + private function recursiveStrongConnect(Vertex $v) + { + $this->indexMap[$v] = $this->index; + $this->lowLinkMap[$v] = $this->index; + $this->index++; + array_push($this->stack, $v); + + // Consider successors of v + foreach ($v->getVerticesEdgeTo() as $w) { + if (! isset($this->indexMap[$w]) ) { + // Successor w has not yet been visited; recurse on it + $this->recursiveStrongConnect($w); + $this->lowLinkMap[$v] = min(array($this->lowLinkMap[$v], $this->lowLinkMap[$w])); + } elseif (in_array($w, $this->stack)) { + // Successor w is in stack S and hence in the current SCC + $this->lowLinkMap[$v] = min(array($this->lowLinkMap[$v], $this->indexMap[$w])); + } + } + // If v is a root node, pop the stack and generate an SCC + if ($this->lowLinkMap[$v] === $this->indexMap[$v]) { + $scc = array(); + do { + $w = array_pop($this->stack); + array_push($scc, $w); + } while ($w !== $v); + + if (count($scc)) { + $this->partition[] = new Vertices($scc); + } + } + } +} diff --git a/tests/TarjanTest.php b/tests/TarjanTest.php new file mode 100644 index 0000000..7ec765a --- /dev/null +++ b/tests/TarjanTest.php @@ -0,0 +1,86 @@ +graph = new Graph(); + } + + public function testTwoCycles() + { + // Build a graph + for ($k = 0; $k < 6; $k++) { + $this->graph->createVertex($k); + } + $vertex = $this->graph->getVertices()->getVector(); + for ($offset = 0; $offset < 6; $offset += 3) { + for ($k = 0; $k < 3; $k++) { + $start = $vertex[$offset + $k]; + $end = $vertex[$offset + (($k + 1) % 3)]; + $start->createEdgeTo($end); + } + } + + // Run the algorithm + $algorithm = new Tarjan(); + + $ret = $algorithm->getStronglyConnectedVerticesFromDirectedGraph($this->graph); + $this->assertCount(2, $ret, 'Two cycles'); + $this->assertCount(3, $ret[0]); + $this->assertCount(3, $ret[1]); + } + + public function testCompleteGraph() + { + $card = 6; + + for ($k = 0; $k < $card; $k++) { + $this->graph->createVertex($k); + } + foreach ($this->graph->getVertices()->getVector() as $src) { + foreach ($this->graph->getVertices()->getVector() as $dst) { + if ($src === $dst) + continue; + $src->createEdgeTo($dst); + } + } + + // Run the algorithm + $algorithm = new Tarjan(); + + $ret = $algorithm->getStronglyConnectedVerticesFromDirectedGraph($this->graph); + + $this->assertCount(1, $ret, 'One SCC'); + $this->assertCount($card, $ret[0]); + } + + public function testNotObviousGraph() + { + $a = $this->graph->createVertex('a'); + $b = $this->graph->createVertex('b'); + $c = $this->graph->createVertex('c'); + $d = $this->graph->createVertex('d'); + + $a->createEdgeTo($b); + $b->createEdgeTo($d); + $d->createEdgeTo($a); + $d->createEdgeTo($c); + $b->createEdgeTo($c); + + // Run the algorithm + $algorithm = new Tarjan(); + + $ret = $algorithm->getStronglyConnectedVerticesFromDirectedGraph($this->graph); + + $this->assertCount(2, $ret); + $this->assertCount(1, $ret[0]); + $this->assertCount(3, $ret[1]); + } +}