Skip to content

Commit de16e33

Browse files
committed
Merge remote-tracking branch 'origin/master' into add-vertices-class
Conflicts: lib/Fhaculty/Graph/Algorithm/ShortestPath/Base.php lib/Fhaculty/Graph/Algorithm/ShortestPath/BreadthFirst.php lib/Fhaculty/Graph/Algorithm/ShortestPath/Dijkstra.php lib/Fhaculty/Graph/Cycle.php lib/Fhaculty/Graph/Walk.php tests/Fhaculty/Graph/Algorithm/ShortestPath/BreadthFirstTest.php
2 parents 72ea516 + ca08410 commit de16e33

File tree

13 files changed

+574
-128
lines changed

13 files changed

+574
-128
lines changed

src/DetectNegativeCycle.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
use Fhaculty\Graph\Graph;
99
use Fhaculty\Graph\Vertex;
10+
use Fhaculty\Graph\Walk;
1011
use Fhaculty\Graph\Exception\NegativeCycleException;
1112
use Fhaculty\Graph\Algorithm\ShortestPath\MooreBellmanFord as SpMooreBellmanFord;
1213

@@ -34,7 +35,7 @@ public function hasCycleNegative()
3435
/**
3536
* Searches all vertices for the first negative cycle
3637
*
37-
* @return Cycle
38+
* @return Walk
3839
* @throws UnderflowException if there's no negative cycle
3940
* @uses AlgorithmSpMooreBellmanFord::getVertices()
4041
*/
@@ -72,7 +73,7 @@ public function getCycleNegative()
7273
* @return Graph
7374
* @throws Exception if there's no negative cycle
7475
* @uses AlgorithmDetectNegativeCycle::getCycleNegative()
75-
* @uses Cycle::createGraph()
76+
* @uses Walk::createGraph()
7677
*/
7778
public function createGraph()
7879
{

src/MaximumMatching/Flow.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ public function getEdges()
5656
} elseif ($group === $groupB) {
5757
$vertex->createEdgeTo($superSink)->setCapacity(1)->setFlow(0);
5858
} else {
59+
// @codeCoverageIgnoreStart
5960
throw new LogicException('Should not happen. Unknown set: ' + $belongingSet);
61+
// @codeCoverageIgnoreEnd
6062
}
6163
}
6264

src/ShortestPath/Base.php

Lines changed: 111 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,60 @@
44

55
use Fhaculty\Graph\Algorithm\BaseVertex;
66
use Fhaculty\Graph\Walk;
7-
use Fhaculty\Graph\Exception\UnderflowException;
7+
use Fhaculty\Graph\Exception\OutOfBoundsException;
88
use Fhaculty\Graph\Exception\InvalidArgumentException;
99
use Fhaculty\Graph\Vertex;
1010
use Fhaculty\Graph\Edge\Base as Edge;
1111
use Fhaculty\Graph\Set\Edges;
1212
use Fhaculty\Graph\Set\Vertices;
1313

14+
/**
15+
* Abstract base class for shortest path algorithms
16+
*
17+
* This abstract base class provides the base interface for working with
18+
* single-source shortest paths (SSSP).
19+
*
20+
* The shortest path problem is the problem of finding a path between two
21+
* vertices such that the sum of the weights of its constituent edges is
22+
* minimized. The weight of the shortest path is referred to as distance.
23+
*
24+
* A--[10]-------------B---E<--F
25+
* \ /
26+
* \--[4]--C--[2]--D
27+
*
28+
* In the above pictured graph, the distance (weight of the shortest path)
29+
* between A and C is 4, and the shortest path between A and B is "A->C->D->B"
30+
* with a distance (total weight) of 6.
31+
*
32+
* In graph theory, it is usually assumed that a path to an unreachable vertex
33+
* has infinite distance. In the above pictured graph, there's no way path
34+
* from A to F, i.e. vertex F is unreachable from vertex A because of the
35+
* directed edge "E <- F" pointing in the opposite direction. This library
36+
* considers this an Exception instead. So if you're asking for the distance
37+
* between A and F, you'll receive an OutOfBoundsException instead.
38+
*
39+
* In graph theory, it is usually assumed that each vertex has a (pseudo-)path
40+
* to itself with a distance of 0. In order to produce reliable, consistent
41+
* results, this library considers this (pseudo-)path to be non-existant, i.e.
42+
* there's NO "magic" path between A and A. So if you're asking for the distance
43+
* between A and A, you'll receive an OutOfBoundsException instead. This allows
44+
* us to check hether there's a real path between A and A (cycle via other
45+
* vertices) as well as working with loop edges.
46+
*
47+
* @link http://en.wikipedia.org/wiki/Shortest_path_problem
48+
* @link http://en.wikipedia.org/wiki/Tree_%28data_structure%29
49+
* @see ShortestPath\Dijkstra
50+
* @see ShortestPath\MooreBellmanFord which also supports negative Edge weights
51+
* @see ShortestPath\BreadthFirst with does not consider Edge weights, but only the number of hops
52+
*/
1453
abstract class Base extends BaseVertex
1554
{
1655
/**
1756
* get walk (path) from start vertex to given end vertex
1857
*
1958
* @param Vertex $endVertex
2059
* @return Walk
21-
* @throws Exception when there's no walk from start to end vertex
60+
* @throws OutOfBoundsException if there's no path to the given end vertex
2261
* @uses self::getEdgesTo()
2362
* @uses Walk::factoryFromEdges()
2463
*/
@@ -31,10 +70,10 @@ public function getWalkTo(Vertex $endVertex)
3170
* get array of edges (path) from start vertex to given end vertex
3271
*
3372
* @param Vertex $endVertex
34-
* @throws Exception
73+
* @throws OutOfBoundsException if there's no path to the given end vertex
3574
* @return Edges
36-
* @uses AlgorithmSp::getEdges()
37-
* @uses AlgorithmSp::getEdgesToInternal()
75+
* @uses self::getEdges()
76+
* @uses self::getEdgesToInternal()
3877
*/
3978
public function getEdgesTo(Vertex $endVertex)
4079
{
@@ -46,15 +85,15 @@ public function getEdgesTo(Vertex $endVertex)
4685
*
4786
* @param Vertex $endVertex
4887
* @param array $edges array of all input edges to operate on
49-
* @throws Exception
88+
* @throws OutOfBoundsException if there's no path to the given vertex
5089
* @return Edges
51-
* @uses AlgorithmSp::getEdges() if no edges were given
90+
* @uses self::getEdges() if no edges were given
5291
*/
5392
protected function getEdgesToInternal(Vertex $endVertex, array $edges)
5493
{
5594
$currentVertex = $endVertex;
5695
$path = array();
57-
while ($currentVertex !== $this->vertex) {
96+
do {
5897
$pre = NULL;
5998
// check all edges to search for edge that points TO current vertex
6099
foreach ($edges as $edge) {
@@ -68,9 +107,9 @@ protected function getEdgesToInternal(Vertex $endVertex, array $edges)
68107
} // ignore: this edge does not point TO current vertex
69108
}
70109
if ($pre === NULL) {
71-
throw new UnderflowException('No edge leading to vertex');
110+
throw new OutOfBoundsException('No edge leading to vertex');
72111
}
73-
}
112+
} while ($currentVertex !== $this->vertex);
74113

75114
return new Edges(array_reverse($path));
76115
}
@@ -96,7 +135,7 @@ private function sumEdges(Edges $edges)
96135
* get set of all Vertices the given start vertex has a path to
97136
*
98137
* @return Vertices
99-
* @uses AlgorithmSp::getDistanceMap()
138+
* @uses self::getDistanceMap()
100139
*/
101140
public function getVertices()
102141
{
@@ -111,13 +150,31 @@ public function getVertices()
111150
return new Vertices($vertices);
112151
}
113152

153+
/**
154+
* checks whether there's a path from this start vertex to given end vertex
155+
*
156+
* @param Vertex $endVertex
157+
* @return boolean
158+
* @uses self::getEdgesTo()
159+
*/
160+
public function hasVertex(Vertex $vertex)
161+
{
162+
try {
163+
$this->getEdgesTo($vertex);
164+
}
165+
catch (OutOfBoundsException $e) {
166+
return false;
167+
}
168+
return true;
169+
}
170+
114171
/**
115172
* get map of vertex IDs to distance
116173
*
117174
* @return float[]
118-
* @uses AlgorithmSp::getEdges()
119-
* @uses AlgorithmSp::getEdgesToInternal()
120-
* @uses AlgorithmSp::sumEdges()
175+
* @uses self::getEdges()
176+
* @uses self::getEdgesToInternal()
177+
* @uses self::sumEdges()
121178
*/
122179
public function getDistanceMap()
123180
{
@@ -126,7 +183,7 @@ public function getDistanceMap()
126183
foreach ($this->vertex->getGraph()->getVertices()->getMap() as $vid => $vertex) {
127184
try {
128185
$ret[$vid] = $this->sumEdges($this->getEdgesToInternal($vertex, $edges));
129-
} catch (UnderflowException $ignore) {
186+
} catch (OutOfBoundsException $ignore) {
130187
} // ignore vertices that can not be reached
131188
}
132189

@@ -138,9 +195,9 @@ public function getDistanceMap()
138195
*
139196
* @param Vertex $endVertex
140197
* @return float
141-
* @throws Exception if given vertex is invalid or there's no path to given end vertex
142-
* @uses AlgorithmSp::getEdgesTo()
143-
* @uses AlgorithmSp::sumEdges()
198+
* @throws OutOfBoundsException if there's no path to the given end vertex
199+
* @uses self::getEdgesTo()
200+
* @uses self::sumEdges()
144201
*/
145202
public function getDistance(Vertex $endVertex)
146203
{
@@ -150,8 +207,43 @@ public function getDistance(Vertex $endVertex)
150207
/**
151208
* create new resulting graph with only edges on shortest path
152209
*
210+
* The resulting Graph will always represent a tree with the start vertex
211+
* being the root vertex.
212+
*
213+
* For example considering the following input Graph with equal weights on
214+
* each edge:
215+
*
216+
* A----->F
217+
* / \ ^
218+
* / \ /
219+
* / \ /
220+
* | E
221+
* | \
222+
* | \
223+
* B--->C<---D
224+
*
225+
* The resulting shortest path tree Graph will look like this:
226+
*
227+
* A----->F
228+
* / \
229+
* / \
230+
* / \
231+
* | E
232+
* | \
233+
* | \
234+
* B--->C D
235+
*
236+
* Or by just arranging the Vertices slightly different:
237+
*
238+
* A
239+
* /|\
240+
* / | \
241+
* B E \->F
242+
* / |
243+
* C<-/ D
244+
*
153245
* @return Graph
154-
* @uses AlgorithmSp::getEdges()
246+
* @uses self::getEdges()
155247
* @uses Graph::createGraphCloneEdges()
156248
*/
157249
public function createGraph()
@@ -171,8 +263,6 @@ public function createGraph()
171263
protected function getEdgesCheapestPredecesor(array $predecessor)
172264
{
173265
$vertices = $this->vertex->getGraph()->getVertices()->getMap();
174-
// start vertex doesn't have a predecessor
175-
unset($vertices[$this->vertex->getId()]);
176266

177267
$edges = array();
178268
foreach ($vertices as $vid => $vertex) {

src/ShortestPath/BreadthFirst.php

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,23 @@
22

33
namespace Fhaculty\Graph\Algorithm\ShortestPath;
44

5-
6-
use Fhaculty\Graph\Exception\InvalidArgumentException;
7-
use Fhaculty\Graph\Exception\OutOfBoundsException;
8-
use Fhaculty\Graph\Walk;
95
use Fhaculty\Graph\Vertex;
6+
use Fhaculty\Graph\Exception\OutOfBoundsException;
107
use Fhaculty\Graph\Set\Vertices;
118
use \Exception;
129

10+
/**
11+
* Simple breadth-first shortest path algorithm
12+
*
13+
* This algorithm ignores edge weights and operates as a level-order algorithm
14+
* on the number of hops. As such, it considers the path with the least number
15+
* of hops to be shortest.
16+
*
17+
* This is particularly useful your Graph doesn't have Edge weights assigned to
18+
* begin with or if you're merely interested in knowing which Vertices can be
19+
* reached at all (path finding). This avoids running expensive operations to
20+
* determine the actual weight (distance) of a path.
21+
*/
1322
class BreadthFirst extends Base
1423
{
1524
/**
@@ -35,6 +44,8 @@ public function getEdgesMap()
3544
$vertexQueue = array();
3645
$edges = array();
3746

47+
// $edges[$this->vertex->getId()] = array();
48+
3849
$vertexCurrent = $this->vertex;
3950
$edgesCurrent = array();
4051

@@ -61,15 +72,14 @@ public function getEdgesMap()
6172

6273
public function getEdgesTo(Vertex $endVertex)
6374
{
64-
if ($endVertex->getGraph() !== $this->vertex->getGraph()) {
65-
throw new InvalidArgumentException('Given target vertex does not belong to the same graph instance');
66-
}
67-
$map = $this->getEdgesMap();
68-
if (!isset($map[$endVertex->getId()])) {
69-
throw new OutOfBoundsException('Given target vertex can not be reached from start vertex');
70-
}
75+
if ($endVertex->getGraph() === $this->vertex->getGraph()) {
76+
$map = $this->getEdgesMap();
7177

72-
return $map[$endVertex->getId()];
78+
if (isset($map[$endVertex->getId()])) {
79+
return $map[$endVertex->getId()];
80+
}
81+
}
82+
throw new OutOfBoundsException('Given target vertex can not be reached from start vertex');
7383
}
7484

7585
/**
@@ -88,28 +98,11 @@ public function getDistanceMap()
8898
return $ret;
8999
}
90100

91-
/**
92-
* checks whether there's a path from this start vertex to given end vertex
93-
*
94-
* @param Vertex $endVertex
95-
* @return boolean
96-
* @uses AlgorithmSpBreadthFirst::getEdgesMap()
97-
*/
98-
public function hasVertex(Vertex $endVertex)
99-
{
100-
if ($endVertex->getGraph() !== $this->vertex->getGraph()) {
101-
throw new InvalidArgumentException('Given target vertex does not belong to the same graph instance');
102-
}
103-
$map = $this->getEdgesMap();
104-
105-
return isset($map[$endVertex->getId()]);
106-
}
107-
108101
/**
109102
* get array of all target vertices this vertex has a path to
110103
*
111104
* @return Vertices
112-
* @uses AlgorithmSpBreadthFirst::getDistanceMap()
105+
* @uses self::getEdgesMap()
113106
*/
114107
public function getVertices()
115108
{

0 commit comments

Comments
 (0)