diff --git a/CHANGELOG.md b/CHANGELOG.md index c763256c..6bd3732a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)). diff --git a/lib/Fhaculty/Graph/Algorithm/MinimumSpanningTree/Base.php b/lib/Fhaculty/Graph/Algorithm/MinimumSpanningTree/Base.php index 6e0f7981..3448a7e9 100644 --- a/lib/Fhaculty/Graph/Algorithm/MinimumSpanningTree/Base.php +++ b/lib/Fhaculty/Graph/Algorithm/MinimumSpanningTree/Base.php @@ -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()); + } + } + } } diff --git a/lib/Fhaculty/Graph/Algorithm/MinimumSpanningTree/Kruskal.php b/lib/Fhaculty/Graph/Algorithm/MinimumSpanningTree/Kruskal.php index b3839dd0..26095cc7 100644 --- a/lib/Fhaculty/Graph/Algorithm/MinimumSpanningTree/Kruskal.php +++ b/lib/Fhaculty/Graph/Algorithm/MinimumSpanningTree/Kruskal.php @@ -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; @@ -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(); diff --git a/lib/Fhaculty/Graph/Algorithm/MinimumSpanningTree/Prim.php b/lib/Fhaculty/Graph/Algorithm/MinimumSpanningTree/Prim.php index f1896fb0..adaa1fc5 100644 --- a/lib/Fhaculty/Graph/Algorithm/MinimumSpanningTree/Prim.php +++ b/lib/Fhaculty/Graph/Algorithm/MinimumSpanningTree/Prim.php @@ -40,15 +40,7 @@ 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 { @@ -56,14 +48,14 @@ public function getEdges() $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()]))); diff --git a/tests/Fhaculty/Graph/Algorithm/MinimumSpanningTree/BaseMstTest.php b/tests/Fhaculty/Graph/Algorithm/MinimumSpanningTree/BaseMstTest.php new file mode 100644 index 00000000..7992cafe --- /dev/null +++ b/tests/Fhaculty/Graph/Algorithm/MinimumSpanningTree/BaseMstTest.php @@ -0,0 +1,169 @@ +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(); + } + + +} \ No newline at end of file diff --git a/tests/Fhaculty/Graph/Algorithm/MinimumSpanningTree/KruskalTest.php b/tests/Fhaculty/Graph/Algorithm/MinimumSpanningTree/KruskalTest.php new file mode 100644 index 00000000..20b31f76 --- /dev/null +++ b/tests/Fhaculty/Graph/Algorithm/MinimumSpanningTree/KruskalTest.php @@ -0,0 +1,24 @@ +getGraph()); + } + + /** + * @expectedException UnexpectedValueException + */ + public function testNullGraphIsNotConsideredToBeConnected() + { + $graph = new Graph(); + + $alg = new Kruskal($graph); + $alg->getEdges(); + } +} diff --git a/tests/Fhaculty/Graph/Algorithm/MinimumSpanningTree/PrimTest.php b/tests/Fhaculty/Graph/Algorithm/MinimumSpanningTree/PrimTest.php index 1102bad3..c69bb6bf 100644 --- a/tests/Fhaculty/Graph/Algorithm/MinimumSpanningTree/PrimTest.php +++ b/tests/Fhaculty/Graph/Algorithm/MinimumSpanningTree/PrimTest.php @@ -1,20 +1,12 @@ assertCount(4, $this->getResultForComplete(5)); - } +use Fhaculty\Graph\Vertex; +use Fhaculty\Graph\Algorithm\MinimumSpanningTree\Prim; - protected function getResultForComplete($n) +class PrimTest extends BaseMstTest +{ + protected function createAlg(Vertex $vertex) { - $loader = new LoaderCompleteGraph($n); - $loader->setEnableDirectedEdges(false); - $alg = new AlgorithmMSTPrim($loader->createGraph()->getVertex(1)); - - return $alg->getEdges(); + return new Prim($vertex); } }