Skip to content

Commit

Permalink
Merge pull request #73 from clue/improve-minimumspanningtree
Browse files Browse the repository at this point in the history
Improve Algorithm\MinimumSpanningTree
  • Loading branch information
clue committed Sep 11, 2013
2 parents 6eb0496 + 79ab285 commit 3e61948
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 57 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ you spot any mistakes.
* Feature: Support opening GraphViz images on Mac OS X in default image viewer
([#67](https://github.com/clue/graph/issues/67) @onigoetz)

* Feature: Add `Algorithm\MinimumSpanningTree\Base::getWeight()` to get total
weight of resulting minimum spanning tree (MST).
([#73](https://github.com/clue/graph/issues/73))

* Feature: Each `Algorithm\MinimumSpanningTree` algorithm now supports
undirected and mixed Graphs, as well as null weights for Edges.
([#73](https://github.com/clue/graph/issues/73))

* BC break: Each `Algorithm\MinimumSpanningTree` algorithm now throws an
`UnexpectedValueException` for unconnected Graphs (and thus also null Graphs).
([#73](https://github.com/clue/graph/issues/73))

* Feature: Add `Walk::factoryFromVertices()`
([#64](https://github.com/clue/graph/issues/64)).

Expand Down
68 changes: 64 additions & 4 deletions lib/Fhaculty/Graph/Algorithm/MinimumSpanningTree/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,88 @@

use Fhaculty\Graph\Algorithm\Base as AlgorithmBase;
use Fhaculty\Graph\Set\Edges;
use Fhaculty\Graph\Edge\Directed as EdgeDirected;
use Fhaculty\Graph\Edge\Base as Edge;
use SplPriorityQueue;

/**
* Abstract base class for minimum spanning tree (MST) algorithms
*
* A minimum spanning tree of a graph is a subgraph that is a tree and connects
* all the vertices together while minimizing the total sum of all edges'
* weights.
*
* A spanning tree thus requires a connected graph (single connected component),
* otherwise we can span multiple trees (spanning forest) within each component.
* Because a null graph (a Graph with no vertices) is not considered connected,
* it also can not contain a spanning tree.
*
* Most authors demand that the input graph has to be undirected, whereas this
* library supports also directed and mixed graphs. The actual direction of the
* edge will be ignored, only its incident vertices will be checked. This is
* done in order to be consistent to how ConnectedComponents are checked.
*
* @link http://en.wikipedia.org/wiki/Minimum_Spanning_Tree
* @link http://en.wikipedia.org/wiki/Spanning_Tree
* @link http://mathoverflow.net/questions/120536/is-the-empty-graph-a-tree
*/
abstract class Base extends AlgorithmBase
{
/**
* create new resulting graph with only edges on minimum spanning tree
*
* @return Graph
* @uses AlgorithmMst::getEdges()
* @uses self::getGraph()
* @uses self::getEdges()
* @uses Graph::createGraphCloneEdges()
*/
public function createGraph()
{
// Copy Graph
return $this->getGraph()->createGraphCloneEdges($this->getEdges());
}

abstract protected function getGraph();

/**
* get all edges on minimum spanning tree
*
* @return Edges
*/
abstract public function getEdges();

/**
* return reference to current Graph
*
* @return Graph
*/
abstract protected function getGraph();

/**
* get total weight of minimum spanning tree
*
* @return float
*/
public function getWeight()
{
return $this->getEdges()->getSumCallback(function (Edge $edge) {
return $edge->getWeight();
});
}

/**
* helper method to add a set of Edges to the given set of sorted edges
*
* @param Edges $edges
* @param SplPriorityQueue $sortedEdges
*/
protected function addEdgesSorted(Edges $edges, SplPriorityQueue $sortedEdges)
{
// For all edges
foreach ($edges as $edge) {
/* @var $edge Edge */
// ignore loops (a->a)
if (!$edge->isLoop()) {
// Add edges with negative weight because of order in stl
$sortedEdges->insert($edge, -$edge->getWeight());
}
}
}
}
27 changes: 1 addition & 26 deletions lib/Fhaculty/Graph/Algorithm/MinimumSpanningTree/Kruskal.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@ public function __construct(Graph $inputGraph)
$this->graph = $inputGraph;
}

public function createGraph()
{
// Copy Graph
return $this->graph->createGraphCloneEdges($this->getEdges());
}

protected function getGraph()
{
return $this->graph;
Expand All @@ -46,26 +40,7 @@ public function getEdges()
$sortedEdges = new SplPriorityQueue();

// For all edges
foreach ($this->graph->getEdges() as $edge) {
// ignore loops (a->a)
if (!$edge->isLoop()) {
if ($edge instanceof EdgeDirected) {
throw new UnexpectedValueException('Kruskal for directed edges not supported');
}
$weight = $edge->getWeight();
if ($weight === NULL) {
throw new UnexpectedValueException('Kruskal for edges with no weight not supported');
}
// Add edges with negativ Weight because of order in stl
$sortedEdges->insert($edge, - $weight);
}
}

if ($sortedEdges->isEmpty()) {
throw new RuntimeException('No edges found');
}

// $sortedEdges = $this->graph->getEdgesOrdered('weight');
$this->addEdgesSorted($this->graph->getEdges(), $sortedEdges);

$returnEdges = array();

Expand Down
18 changes: 5 additions & 13 deletions lib/Fhaculty/Graph/Algorithm/MinimumSpanningTree/Prim.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,30 +40,22 @@ public function getEdges()

// get unvisited vertex of the edge and add edges from new vertex
// Add all edges from $currentVertex to priority queue
foreach ($vertexCurrent->getEdges() as $currentEdge) {
if (!$currentEdge->isLoop()) {
if ($currentEdge instanceof EdgeDirected) {
throw new UnexpectedValueException('Unable to create MST for directed graphs');
}
// Add edges to priority queue with inverted weights (priority queue has high values at the front)
$edgeQueue->insert($currentEdge, -$currentEdge->getWeight());
}
}
$this->addEdgesSorted($vertexCurrent->getEdges(), $edgeQueue);

do {
try {
// Get next cheapest edge
$cheapestEdge = $edgeQueue->extract();
/* @var $cheapestEdge EdgeDirected */
} catch (Exception $e) {
return $returnEdges;
throw new UnexpectedValueException('Graph has more than one component');
throw new UnexpectedValueException('Graph has more than one component', 0, $e);
}

// Check if edge is between unmarked and marked edge

$vertexA = $cheapestEdge->getVerticesStart()->getVertexFirst();
$vertexB = $cheapestEdge->getVertexToFrom($vertexA);
$vertices = $cheapestEdge->getVertices();
$vertexA = $vertices->getVertexFirst();
$vertexB = $vertices->getVertexLast();

// Edge is between marked and unmared vertex
} while (!(isset($markInserted[$vertexA->getId()]) XOR isset($markInserted[$vertexB->getId()])));
Expand Down
169 changes: 169 additions & 0 deletions tests/Fhaculty/Graph/Algorithm/MinimumSpanningTree/BaseMstTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php

use Fhaculty\Graph\Graph;

use Fhaculty\Graph\Vertex;
use Fhaculty\Graph\Loader\CompleteGraph as LoaderCompleteGraph;
use Fhaculty\Graph\Algorithm\MinimumSpanningTree\Base as MstBase;

abstract class BaseMstTest extends TestCase
{
/**
* @param Vertex $vertex
* @return MstBase
*/
abstract protected function createAlg(Vertex $vertex);

public function testIsolatedVertex()
{
$graph = new Graph();
$v1 = $graph->createVertex(1);

$alg = $this->createAlg($v1);

$this->assertCount(0, $alg->getEdges());
$this->assertEquals(0, $alg->getWeight());

$graphMst = $alg->createGraph();
$this->assertGraphEquals($graph, $graphMst);
}

public function testSingleEdge()
{
// 1 --[3]-- 2
$graph = new Graph();
$v1 = $graph->createVertex(1);
$v2 = $graph->createVertex(2);
$v1->createEdge($v2)->setWeight(3);

$alg = $this->createAlg($v1);

$this->assertCount(1, $alg->getEdges());
$this->assertEquals(3, $alg->getWeight());
$this->assertGraphEquals($graph, $alg->createGraph());
}

public function testSimpleGraph()
{
// 1 --[6]-- 2 --[9]-- 3 --[7]-- 4 --[8]-- 5
$graph = new Graph();
$v1 = $graph->createVertex(1);
$v2 = $graph->createVertex(2);
$v3 = $graph->createVertex(3);
$v4 = $graph->createVertex(4);
$v5 = $graph->createVertex(5);
$v1->createEdge($v2)->setWeight(6);
$v2->createEdge($v3)->setWeight(9);
$v3->createEdge($v4)->setWeight(7);
$v4->createEdge($v5)->setWeight(8);

$alg = $this->createAlg($v1);

$graphMst = $alg->createGraph();
$this->assertGraphEquals($graph, $graphMst);
}

public function testFindingCheapestEdge()
{
// /--[4]--\
// / \
// 1 ---[3]--- 2
// \ /
// \--[5]--/
$graph = new Graph();
$v1 = $graph->createVertex(1);
$v2 = $graph->createVertex(2);
$v1->createEdge($v2)->setWeight(4);
$v1->createEdge($v2)->setWeight(3);
$v1->createEdge($v2)->setWeight(5);

$alg = $this->createAlg($v1);
$edges = $alg->getEdges();

$this->assertCount(1, $edges);
$this->assertEquals(3, $edges->getEdgeFirst()->getWeight());
$this->assertEquals(3, $alg->getWeight());
}

public function testFindingCheapestTree()
{
// 1 --[4]-- 2 --[5]-- 3
// \ /
// \-------[6]-----/
$graph = new Graph();
$v1 = $graph->createVertex(1);
$v2 = $graph->createVertex(2);
$v3 = $graph->createVertex(3);
$v1->createEdge($v2)->setWeight(4);
$v2->createEdge($v3)->setWeight(5);
$v3->createEdge($v1)->setWeight(6);

// 1 --[4]-- 2 -- [5] -- 3
$graphExpected = new Graph();
$ve1 = $graphExpected->createVertex(1);
$ve2 = $graphExpected->createVertex(2);
$ve3 = $graphExpected->createVertex(3);
$ve1->createEdge($ve2)->setWeight(4);
$ve2->createEdge($ve3)->setWeight(5);

$alg = $this->createAlg($v1);
$this->assertCount(2, $alg->getEdges());
$this->assertEquals(9, $alg->getWeight());
$this->assertGraphEquals($graphExpected, $alg->createGraph());
}

public function testMixedGraphDirectionIsIgnored()
{
// 1 --[6]-> 2 --[7]-- 3 --[8]-- 4 <-[9]-- 5
$graph = new Graph();
$v1 = $graph->createVertex(1);
$v2 = $graph->createVertex(2);
$v3 = $graph->createVertex(3);
$v4 = $graph->createVertex(4);
$v5 = $graph->createVertex(5);
$v1->createEdgeTo($v2)->setWeight(6);
$v2->createEdge($v3)->setWeight(7);
$v4->createEdge($v3)->setWeight(8);
$v5->createEdgeTo($v4)->setWeight(9);

$alg = $this->createAlg($v1);

$this->assertCount(4, $alg->getEdges());
$this->assertEquals(30, $alg->getWeight());
$this->assertGraphEquals($graph, $alg->createGraph());
}

/**
* @expectedException UnexpectedValueException
*/
public function testMultipleComponentsFail()
{
// 1 --[1]-- 2, 3 --[1]-- 4
$graph = new Graph();
$v1 = $graph->createVertex(1);
$v2 = $graph->createVertex(2);
$v3 = $graph->createVertex(3);
$v4 = $graph->createVertex(4);
$v1->createEdge($v2)->setWeight(1);
$v3->createEdge($v4)->setWeight(1);

$alg = $this->createAlg($v1);
$alg->getEdges();
}

/**
* @expectedException UnexpectedValueException
*/
public function testMultipleIsolatedVerticesFormMultipleComponentsFail()
{
// 1, 2
$graph = new Graph();
$v1 = $graph->createVertex(1);
$v2 = $graph->createVertex(2);

$alg = $this->createAlg($v1);
$alg->getEdges();
}


}
24 changes: 24 additions & 0 deletions tests/Fhaculty/Graph/Algorithm/MinimumSpanningTree/KruskalTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

use Fhaculty\Graph\Graph;
use Fhaculty\Graph\Vertex;
use Fhaculty\Graph\Algorithm\MinimumSpanningTree\Kruskal;

class KruskalTest extends BaseMstTest
{
protected function createAlg(Vertex $vertex)
{
return new Kruskal($vertex->getGraph());
}

/**
* @expectedException UnexpectedValueException
*/
public function testNullGraphIsNotConsideredToBeConnected()
{
$graph = new Graph();

$alg = new Kruskal($graph);
$alg->getEdges();
}
}
Loading

0 comments on commit 3e61948

Please sign in to comment.