-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tarjan's strongly connected component algorithm #14
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
<?php | ||
|
||
namespace Graphp\Algorithms; | ||
|
||
use Fhaculty\Graph\Exception\InvalidArgumentException; | ||
use Fhaculty\Graph\Graph; | ||
use Fhaculty\Graph\Set\Vertices; | ||
use Fhaculty\Graph\Vertex; | ||
|
||
/** | ||
* Finds the Strongly Connected Components in a Directed Graph, a graph component being said to be strongly connected | ||
* if every vertex is reachable from every other vertex. | ||
* | ||
* More information here: | ||
* @see https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm | ||
* | ||
* This code was inspired by: | ||
* @see http://github.com/Trismegiste/Mondrian/blob/master/Graph/Tarjan.php and | ||
* @see https://code.google.com/p/jbpt/source/browse/trunk/jbpt-core/src/main/java/org/jbpt/algo/graph/StronglyConnectedComponents.java | ||
*/ | ||
class Tarjan | ||
{ | ||
/** @var \SplObjectStorage */ | ||
private $indexMap; | ||
|
||
/** @var \SplObjectStorage */ | ||
private $lowLinkMap; | ||
|
||
/** @var Vertex[] */ | ||
private $stack; | ||
|
||
/** @var int */ | ||
private $index; | ||
|
||
/** @var Vertices[] */ | ||
private $partition; | ||
|
||
/** | ||
* Get the strongly connected components of this digraph by the Tarjan algorithm. | ||
* | ||
* @param Graph $graph Graph to analyze | ||
* @throws InvalidArgumentException For undirected graph argument. | ||
* @return Vertices[] Array of Strongly Connected components. | ||
*/ | ||
public function getStronglyConnectedVerticesFromDirectedGraph(Graph $graph) | ||
{ | ||
// check is directed | ||
$directed = new Directed($graph); | ||
if ($directed->hasUndirected()) { | ||
throw new InvalidArgumentException('Graph shall be directed'); | ||
} | ||
|
||
$this->stack = array(); | ||
$this->index = 0; | ||
$this->partition = array(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both index maps (see ctor) will not be reset, is this intentional? Afaict this means that running the algorithm again will result in a different result? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right, but since we were working on a single Graph (passed in the ctor), I was thinking of this class as fixed input. I reworked this part as requested, there is no more ctor, you pass directly the graph instance to the main method. |
||
$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); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
<?php | ||
|
||
use Fhaculty\Graph\Graph; | ||
use Fhaculty\Graph\Vertex; | ||
use Graphp\Algorithms\Tarjan; | ||
|
||
class TarjanTest extends TestCase | ||
{ | ||
protected $graph; | ||
|
||
protected function setUp() | ||
{ | ||
$this->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); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does this do? :-) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO this looks a bit overkill, perhaps we can use a simpler test case? |
||
|
||
// 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); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks familiar (#6) :-) How about using a simpler test case like a hard-coded K_2? |
||
|
||
// 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apparently this is "not obvious"? :-) How about using a "more obvious" example? :-) |
||
|
||
// 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]); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naming this method is indeed a tough challenge :-)
Afaict this appears to be the first method so far which returns not only an array of vertex instances, but an array of vertices 😮
Admittedly, I'm not a big fan of the current nor the suggested naming convention. The current suggested name matches this rather elaborate style, how about using something more concise?
Does naming this "TarjansAlgorithm::getConnectedComponents(Graph)" make sense to you?