Skip to content
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

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions src/Tarjan.php
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)
Copy link
Member

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?

{
// 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();
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The 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);
}
}
}
}
86 changes: 86 additions & 0 deletions tests/TarjanTest.php
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);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this do? :-)

Copy link
Member

Choose a reason for hiding this comment

The 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);
}
}
Copy link
Member

Choose a reason for hiding this comment

The 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);
Copy link
Member

Choose a reason for hiding this comment

The 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]);
}
}