Skip to content

Commit ea2516d

Browse files
committed
Merge pull request #73 from clue/improve-minimumspanningtree
Improve Algorithm\MinimumSpanningTree
2 parents 20f700a + dba92ef commit ea2516d

File tree

6 files changed

+269
-57
lines changed

6 files changed

+269
-57
lines changed

src/MinimumSpanningTree/Base.php

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,88 @@
44

55
use Fhaculty\Graph\Algorithm\Base as AlgorithmBase;
66
use Fhaculty\Graph\Set\Edges;
7+
use Fhaculty\Graph\Edge\Directed as EdgeDirected;
8+
use Fhaculty\Graph\Edge\Base as Edge;
9+
use SplPriorityQueue;
710

11+
/**
12+
* Abstract base class for minimum spanning tree (MST) algorithms
13+
*
14+
* A minimum spanning tree of a graph is a subgraph that is a tree and connects
15+
* all the vertices together while minimizing the total sum of all edges'
16+
* weights.
17+
*
18+
* A spanning tree thus requires a connected graph (single connected component),
19+
* otherwise we can span multiple trees (spanning forest) within each component.
20+
* Because a null graph (a Graph with no vertices) is not considered connected,
21+
* it also can not contain a spanning tree.
22+
*
23+
* Most authors demand that the input graph has to be undirected, whereas this
24+
* library supports also directed and mixed graphs. The actual direction of the
25+
* edge will be ignored, only its incident vertices will be checked. This is
26+
* done in order to be consistent to how ConnectedComponents are checked.
27+
*
28+
* @link http://en.wikipedia.org/wiki/Minimum_Spanning_Tree
29+
* @link http://en.wikipedia.org/wiki/Spanning_Tree
30+
* @link http://mathoverflow.net/questions/120536/is-the-empty-graph-a-tree
31+
*/
832
abstract class Base extends AlgorithmBase
933
{
1034
/**
1135
* create new resulting graph with only edges on minimum spanning tree
1236
*
1337
* @return Graph
14-
* @uses AlgorithmMst::getEdges()
38+
* @uses self::getGraph()
39+
* @uses self::getEdges()
1540
* @uses Graph::createGraphCloneEdges()
1641
*/
1742
public function createGraph()
1843
{
19-
// Copy Graph
2044
return $this->getGraph()->createGraphCloneEdges($this->getEdges());
2145
}
2246

23-
abstract protected function getGraph();
24-
2547
/**
2648
* get all edges on minimum spanning tree
2749
*
2850
* @return Edges
2951
*/
3052
abstract public function getEdges();
53+
54+
/**
55+
* return reference to current Graph
56+
*
57+
* @return Graph
58+
*/
59+
abstract protected function getGraph();
60+
61+
/**
62+
* get total weight of minimum spanning tree
63+
*
64+
* @return float
65+
*/
66+
public function getWeight()
67+
{
68+
return $this->getEdges()->getSumCallback(function (Edge $edge) {
69+
return $edge->getWeight();
70+
});
71+
}
72+
73+
/**
74+
* helper method to add a set of Edges to the given set of sorted edges
75+
*
76+
* @param Edges $edges
77+
* @param SplPriorityQueue $sortedEdges
78+
*/
79+
protected function addEdgesSorted(Edges $edges, SplPriorityQueue $sortedEdges)
80+
{
81+
// For all edges
82+
foreach ($edges as $edge) {
83+
/* @var $edge Edge */
84+
// ignore loops (a->a)
85+
if (!$edge->isLoop()) {
86+
// Add edges with negative weight because of order in stl
87+
$sortedEdges->insert($edge, -$edge->getWeight());
88+
}
89+
}
90+
}
3191
}

src/MinimumSpanningTree/Kruskal.php

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,6 @@ public function __construct(Graph $inputGraph)
2424
$this->graph = $inputGraph;
2525
}
2626

27-
public function createGraph()
28-
{
29-
// Copy Graph
30-
return $this->graph->createGraphCloneEdges($this->getEdges());
31-
}
32-
3327
protected function getGraph()
3428
{
3529
return $this->graph;
@@ -46,26 +40,7 @@ public function getEdges()
4640
$sortedEdges = new SplPriorityQueue();
4741

4842
// For all edges
49-
foreach ($this->graph->getEdges() as $edge) {
50-
// ignore loops (a->a)
51-
if (!$edge->isLoop()) {
52-
if ($edge instanceof EdgeDirected) {
53-
throw new UnexpectedValueException('Kruskal for directed edges not supported');
54-
}
55-
$weight = $edge->getWeight();
56-
if ($weight === NULL) {
57-
throw new UnexpectedValueException('Kruskal for edges with no weight not supported');
58-
}
59-
// Add edges with negativ Weight because of order in stl
60-
$sortedEdges->insert($edge, - $weight);
61-
}
62-
}
63-
64-
if ($sortedEdges->isEmpty()) {
65-
throw new RuntimeException('No edges found');
66-
}
67-
68-
// $sortedEdges = $this->graph->getEdgesOrdered('weight');
43+
$this->addEdgesSorted($this->graph->getEdges(), $sortedEdges);
6944

7045
$returnEdges = array();
7146

src/MinimumSpanningTree/Prim.php

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,30 +40,22 @@ public function getEdges()
4040

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

5345
do {
5446
try {
5547
// Get next cheapest edge
5648
$cheapestEdge = $edgeQueue->extract();
5749
/* @var $cheapestEdge EdgeDirected */
5850
} catch (Exception $e) {
59-
return $returnEdges;
60-
throw new UnexpectedValueException('Graph has more than one component');
51+
throw new UnexpectedValueException('Graph has more than one component', 0, $e);
6152
}
6253

6354
// Check if edge is between unmarked and marked edge
6455

65-
$vertexA = $cheapestEdge->getVerticesStart()->getVertexFirst();
66-
$vertexB = $cheapestEdge->getVertexToFrom($vertexA);
56+
$vertices = $cheapestEdge->getVertices();
57+
$vertexA = $vertices->getVertexFirst();
58+
$vertexB = $vertices->getVertexLast();
6759

6860
// Edge is between marked and unmared vertex
6961
} while (!(isset($markInserted[$vertexA->getId()]) XOR isset($markInserted[$vertexB->getId()])));
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<?php
2+
3+
use Fhaculty\Graph\Graph;
4+
5+
use Fhaculty\Graph\Vertex;
6+
use Fhaculty\Graph\Loader\CompleteGraph as LoaderCompleteGraph;
7+
use Fhaculty\Graph\Algorithm\MinimumSpanningTree\Base as MstBase;
8+
9+
abstract class BaseMstTest extends TestCase
10+
{
11+
/**
12+
* @param Vertex $vertex
13+
* @return MstBase
14+
*/
15+
abstract protected function createAlg(Vertex $vertex);
16+
17+
public function testIsolatedVertex()
18+
{
19+
$graph = new Graph();
20+
$v1 = $graph->createVertex(1);
21+
22+
$alg = $this->createAlg($v1);
23+
24+
$this->assertCount(0, $alg->getEdges());
25+
$this->assertEquals(0, $alg->getWeight());
26+
27+
$graphMst = $alg->createGraph();
28+
$this->assertGraphEquals($graph, $graphMst);
29+
}
30+
31+
public function testSingleEdge()
32+
{
33+
// 1 --[3]-- 2
34+
$graph = new Graph();
35+
$v1 = $graph->createVertex(1);
36+
$v2 = $graph->createVertex(2);
37+
$v1->createEdge($v2)->setWeight(3);
38+
39+
$alg = $this->createAlg($v1);
40+
41+
$this->assertCount(1, $alg->getEdges());
42+
$this->assertEquals(3, $alg->getWeight());
43+
$this->assertGraphEquals($graph, $alg->createGraph());
44+
}
45+
46+
public function testSimpleGraph()
47+
{
48+
// 1 --[6]-- 2 --[9]-- 3 --[7]-- 4 --[8]-- 5
49+
$graph = new Graph();
50+
$v1 = $graph->createVertex(1);
51+
$v2 = $graph->createVertex(2);
52+
$v3 = $graph->createVertex(3);
53+
$v4 = $graph->createVertex(4);
54+
$v5 = $graph->createVertex(5);
55+
$v1->createEdge($v2)->setWeight(6);
56+
$v2->createEdge($v3)->setWeight(9);
57+
$v3->createEdge($v4)->setWeight(7);
58+
$v4->createEdge($v5)->setWeight(8);
59+
60+
$alg = $this->createAlg($v1);
61+
62+
$graphMst = $alg->createGraph();
63+
$this->assertGraphEquals($graph, $graphMst);
64+
}
65+
66+
public function testFindingCheapestEdge()
67+
{
68+
// /--[4]--\
69+
// / \
70+
// 1 ---[3]--- 2
71+
// \ /
72+
// \--[5]--/
73+
$graph = new Graph();
74+
$v1 = $graph->createVertex(1);
75+
$v2 = $graph->createVertex(2);
76+
$v1->createEdge($v2)->setWeight(4);
77+
$v1->createEdge($v2)->setWeight(3);
78+
$v1->createEdge($v2)->setWeight(5);
79+
80+
$alg = $this->createAlg($v1);
81+
$edges = $alg->getEdges();
82+
83+
$this->assertCount(1, $edges);
84+
$this->assertEquals(3, $edges->getEdgeFirst()->getWeight());
85+
$this->assertEquals(3, $alg->getWeight());
86+
}
87+
88+
public function testFindingCheapestTree()
89+
{
90+
// 1 --[4]-- 2 --[5]-- 3
91+
// \ /
92+
// \-------[6]-----/
93+
$graph = new Graph();
94+
$v1 = $graph->createVertex(1);
95+
$v2 = $graph->createVertex(2);
96+
$v3 = $graph->createVertex(3);
97+
$v1->createEdge($v2)->setWeight(4);
98+
$v2->createEdge($v3)->setWeight(5);
99+
$v3->createEdge($v1)->setWeight(6);
100+
101+
// 1 --[4]-- 2 -- [5] -- 3
102+
$graphExpected = new Graph();
103+
$ve1 = $graphExpected->createVertex(1);
104+
$ve2 = $graphExpected->createVertex(2);
105+
$ve3 = $graphExpected->createVertex(3);
106+
$ve1->createEdge($ve2)->setWeight(4);
107+
$ve2->createEdge($ve3)->setWeight(5);
108+
109+
$alg = $this->createAlg($v1);
110+
$this->assertCount(2, $alg->getEdges());
111+
$this->assertEquals(9, $alg->getWeight());
112+
$this->assertGraphEquals($graphExpected, $alg->createGraph());
113+
}
114+
115+
public function testMixedGraphDirectionIsIgnored()
116+
{
117+
// 1 --[6]-> 2 --[7]-- 3 --[8]-- 4 <-[9]-- 5
118+
$graph = new Graph();
119+
$v1 = $graph->createVertex(1);
120+
$v2 = $graph->createVertex(2);
121+
$v3 = $graph->createVertex(3);
122+
$v4 = $graph->createVertex(4);
123+
$v5 = $graph->createVertex(5);
124+
$v1->createEdgeTo($v2)->setWeight(6);
125+
$v2->createEdge($v3)->setWeight(7);
126+
$v4->createEdge($v3)->setWeight(8);
127+
$v5->createEdgeTo($v4)->setWeight(9);
128+
129+
$alg = $this->createAlg($v1);
130+
131+
$this->assertCount(4, $alg->getEdges());
132+
$this->assertEquals(30, $alg->getWeight());
133+
$this->assertGraphEquals($graph, $alg->createGraph());
134+
}
135+
136+
/**
137+
* @expectedException UnexpectedValueException
138+
*/
139+
public function testMultipleComponentsFail()
140+
{
141+
// 1 --[1]-- 2, 3 --[1]-- 4
142+
$graph = new Graph();
143+
$v1 = $graph->createVertex(1);
144+
$v2 = $graph->createVertex(2);
145+
$v3 = $graph->createVertex(3);
146+
$v4 = $graph->createVertex(4);
147+
$v1->createEdge($v2)->setWeight(1);
148+
$v3->createEdge($v4)->setWeight(1);
149+
150+
$alg = $this->createAlg($v1);
151+
$alg->getEdges();
152+
}
153+
154+
/**
155+
* @expectedException UnexpectedValueException
156+
*/
157+
public function testMultipleIsolatedVerticesFormMultipleComponentsFail()
158+
{
159+
// 1, 2
160+
$graph = new Graph();
161+
$v1 = $graph->createVertex(1);
162+
$v2 = $graph->createVertex(2);
163+
164+
$alg = $this->createAlg($v1);
165+
$alg->getEdges();
166+
}
167+
168+
169+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
use Fhaculty\Graph\Graph;
4+
use Fhaculty\Graph\Vertex;
5+
use Fhaculty\Graph\Algorithm\MinimumSpanningTree\Kruskal;
6+
7+
class KruskalTest extends BaseMstTest
8+
{
9+
protected function createAlg(Vertex $vertex)
10+
{
11+
return new Kruskal($vertex->getGraph());
12+
}
13+
14+
/**
15+
* @expectedException UnexpectedValueException
16+
*/
17+
public function testNullGraphIsNotConsideredToBeConnected()
18+
{
19+
$graph = new Graph();
20+
21+
$alg = new Kruskal($graph);
22+
$alg->getEdges();
23+
}
24+
}
Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,12 @@
11
<?php
2-
use Fhaculty\Graph\Algorithm\MinimumSpanningTree\Prim as AlgorithmMSTPrim;
3-
use Fhaculty\Graph\Loader\CompleteGraph as LoaderCompleteGraph;
42

5-
class PrimTest extends PHPUnit_Framework_TestCase
6-
{
7-
public function testKnownComplete()
8-
{
9-
$this->assertCount(4, $this->getResultForComplete(5));
10-
}
3+
use Fhaculty\Graph\Vertex;
4+
use Fhaculty\Graph\Algorithm\MinimumSpanningTree\Prim;
115

12-
protected function getResultForComplete($n)
6+
class PrimTest extends BaseMstTest
7+
{
8+
protected function createAlg(Vertex $vertex)
139
{
14-
$loader = new LoaderCompleteGraph($n);
15-
$loader->setEnableDirectedEdges(false);
16-
$alg = new AlgorithmMSTPrim($loader->createGraph()->getVertex(1));
17-
18-
return $alg->getEdges();
10+
return new Prim($vertex);
1911
}
2012
}

0 commit comments

Comments
 (0)