Skip to content

Commit

Permalink
Merge pull request #62 from clue/improve-shortestpath
Browse files Browse the repository at this point in the history
Improve Algorithm\ShortestPath
  • Loading branch information
clue committed Jul 14, 2013
2 parents 466f79e + 86e67a3 commit be01ca5
Show file tree
Hide file tree
Showing 9 changed files with 486 additions and 123 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ This file is a manually maintained list of changes for each release. Feel free
to add your changes here when sending pull requests. Also send corrections if
you spot any mistakes.

## 0.7.0 (2013-xx-xx)

* BC break: Each `Algorithm\ShortestPath` algorithm now consistenly does not
return a zero weight for the root Vertex and now supports loop edges on the root
Vertex ([#62](https://github.com/clue/graph/issues/62))
* BC break: Each `Algorithm\ShortestPath` algorithm now consistently throws an
`OutOfBoundsException` for unreachable vertices
([#62](https://github.com/clue/graph/issues/62))
* Feature: Add `Algorithm\ShortestPath::hasVertex(Vertex $vertex)` to check whether
a path to the given Vertex exists ([#62](https://github.com/clue/graph/issues/62)).
* Fix: Missing import prevented
`Algorithm\ShortestPath\MooreBellmanFord::getCycleNegative()` from actually
throwing the right `UnderflowException` if no cycle was actually found
([#62](https://github.com/clue/graph/issues/62))

## 0.6.0 (2013-07-11)

* BC break: Move algorithm definitions in base classes to separate algorithm classes ([#27](https://github.com/clue/graph/issues/27)).
Expand Down
137 changes: 112 additions & 25 deletions lib/Fhaculty/Graph/Algorithm/ShortestPath/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,58 @@

use Fhaculty\Graph\Algorithm\BaseVertex;
use Fhaculty\Graph\Walk;

use Fhaculty\Graph\Exception\UnderflowException;

use Fhaculty\Graph\Exception\OutOfBoundsException;
use Fhaculty\Graph\Exception\InvalidArgumentException;

use Fhaculty\Graph\Vertex;
use Fhaculty\Graph\Edge\Base as Edge;

/**
* Abstract base class for shortest path algorithms
*
* This abstract base class provides the base interface for working with
* single-source shortest paths (SSSP).
*
* The shortest path problem is the problem of finding a path between two
* vertices such that the sum of the weights of its constituent edges is
* minimized. The weight of the shortest path is referred to as distance.
*
* A--[10]-------------B---E<--F
* \ /
* \--[4]--C--[2]--D
*
* In the above pictured graph, the distance (weight of the shortest path)
* between A and C is 4, and the shortest path between A and B is "A->C->D->B"
* with a distance (total weight) of 6.
*
* In graph theory, it is usually assumed that a path to an unreachable vertex
* has infinite distance. In the above pictured graph, there's no way path
* from A to F, i.e. vertex F is unreachable from vertex A because of the
* directed edge "E <- F" pointing in the opposite direction. This library
* considers this an Exception instead. So if you're asking for the distance
* between A and F, you'll receive an OutOfBoundsException instead.
*
* In graph theory, it is usually assumed that each vertex has a (pseudo-)path
* to itself with a distance of 0. In order to produce reliable, consistent
* results, this library considers this (pseudo-)path to be non-existant, i.e.
* there's NO "magic" path between A and A. So if you're asking for the distance
* between A and A, you'll receive an OutOfBoundsException instead. This allows
* us to check hether there's a real path between A and A (cycle via other
* vertices) as well as working with loop edges.
*
* @link http://en.wikipedia.org/wiki/Shortest_path_problem
* @link http://en.wikipedia.org/wiki/Tree_%28data_structure%29
* @see ShortestPath\Dijkstra
* @see ShortestPath\MooreBellmanFord which also supports negative Edge weights
* @see ShortestPath\BreadthFirst with does not consider Edge weights, but only the number of hops
*/
abstract class Base extends BaseVertex
{
/**
* get walk (path) from start vertex to given end vertex
*
* @param Vertex $endVertex
* @return Walk
* @throws Exception when there's no walk from start to end vertex
* @throws OutOfBoundsException if there's no path to the given end vertex
* @uses self::getEdgesTo()
* @uses Walk::factoryFromEdges()
*/
Expand All @@ -32,10 +68,10 @@ public function getWalkTo(Vertex $endVertex)
* get array of edges (path) from start vertex to given end vertex
*
* @param Vertex $endVertex
* @throws Exception
* @throws OutOfBoundsException if there's no path to the given end vertex
* @return Edge[]
* @uses AlgorithmSp::getEdges()
* @uses AlgorithmSp::getEdgesToInternal()
* @uses self::getEdges()
* @uses self::getEdgesToInternal()
*/
public function getEdgesTo(Vertex $endVertex)
{
Expand All @@ -47,15 +83,15 @@ public function getEdgesTo(Vertex $endVertex)
*
* @param Vertex $endVertex
* @param array $edges array of all input edges to operate on
* @throws Exception
* @throws OutOfBoundsException if there's no path to the given vertex
* @return Edge[]
* @uses AlgorithmSp::getEdges() if no edges were given
* @uses self::getEdges() if no edges were given
*/
protected function getEdgesToInternal(Vertex $endVertex, array $edges)
{
$currentVertex = $endVertex;
$path = array();
while ($currentVertex !== $this->vertex) {
do {
$pre = NULL;
// check all edges to search for edge that points TO current vertex
foreach ($edges as $edge) {
Expand All @@ -69,9 +105,9 @@ protected function getEdgesToInternal(Vertex $endVertex, array $edges)
} // ignore: this edge does not point TO current vertex
}
if ($pre === NULL) {
throw new UnderflowException('No edge leading to vertex');
throw new OutOfBoundsException('No edge leading to vertex');
}
}
} while ($currentVertex !== $this->vertex);

return array_reverse($path);
}
Expand All @@ -97,7 +133,7 @@ private function sumEdges(array $edges)
* get array of all vertices the given start vertex has a path to
*
* @return Vertex[]
* @uses AlgorithmSp::getDistanceMap()
* @uses self::getDistanceMap()
*/
public function getVertices()
{
Expand All @@ -116,20 +152,38 @@ public function getVertices()
* get array of all vertices' IDs the given start vertex has a path to
*
* @return int[]
* @uses AlgorithmSp::getDistanceMap()
* @uses self::getDistanceMap()
*/
public function getVerticesId()
{
return array_keys($this->getDistanceMap());
}

/**
* checks whether there's a path from this start vertex to given end vertex
*
* @param Vertex $endVertex
* @return boolean
* @uses self::getEdgesTo()
*/
public function hasVertex(Vertex $vertex)
{
try {
$this->getEdgesTo($vertex);
}
catch (OutOfBoundsException $e) {
return false;
}
return true;
}

/**
* get map of vertex IDs to distance
*
* @return float[]
* @uses AlgorithmSp::getEdges()
* @uses AlgorithmSp::getEdgesToInternal()
* @uses AlgorithmSp::sumEdges()
* @uses self::getEdges()
* @uses self::getEdgesToInternal()
* @uses self::sumEdges()
*/
public function getDistanceMap()
{
Expand All @@ -138,7 +192,7 @@ public function getDistanceMap()
foreach ($this->vertex->getGraph()->getVertices() as $vid => $vertex) {
try {
$ret[$vid] = $this->sumEdges($this->getEdgesToInternal($vertex, $edges));
} catch (UnderflowException $ignore) {
} catch (OutOfBoundsException $ignore) {
} // ignore vertices that can not be reached
}

Expand All @@ -150,9 +204,9 @@ public function getDistanceMap()
*
* @param Vertex $endVertex
* @return float
* @throws Exception if given vertex is invalid or there's no path to given end vertex
* @uses AlgorithmSp::getEdgesTo()
* @uses AlgorithmSp::sumEdges()
* @throws OutOfBoundsException if there's no path to the given end vertex
* @uses self::getEdgesTo()
* @uses self::sumEdges()
*/
public function getDistance(Vertex $endVertex)
{
Expand All @@ -162,8 +216,43 @@ public function getDistance(Vertex $endVertex)
/**
* create new resulting graph with only edges on shortest path
*
* The resulting Graph will always represent a tree with the start vertex
* being the root vertex.
*
* For example considering the following input Graph with equal weights on
* each edge:
*
* A----->F
* / \ ^
* / \ /
* / \ /
* | E
* | \
* | \
* B--->C<---D
*
* The resulting shortest path tree Graph will look like this:
*
* A----->F
* / \
* / \
* / \
* | E
* | \
* | \
* B--->C D
*
* Or by just arranging the Vertices slightly different:
*
* A
* /|\
* / | \
* B E \->F
* / |
* C<-/ D
*
* @return Graph
* @uses AlgorithmSp::getEdges()
* @uses self::getEdges()
* @uses Graph::createGraphCloneEdges()
*/
public function createGraph()
Expand All @@ -183,8 +272,6 @@ public function createGraph()
protected function getEdgesCheapestPredecesor(array $predecessor)
{
$vertices = $this->vertex->getGraph()->getVertices();
// start vertex doesn't have a predecessor
unset($vertices[$this->vertex->getId()]);

$edges = array();
foreach ($vertices as $vid => $vertex) {
Expand Down
55 changes: 23 additions & 32 deletions lib/Fhaculty/Graph/Algorithm/ShortestPath/BreadthFirst.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@

namespace Fhaculty\Graph\Algorithm\ShortestPath;

use Fhaculty\Graph\Vertex;
use Fhaculty\Graph\Exception\InvalidArgumentException;

use Fhaculty\Graph\Exception\OutOfBoundsException;

use Fhaculty\Graph\Walk;

use Fhaculty\Graph\Vertex;
use \Exception;

/**
* Simple breadth-first shortest path algorithm
*
* This algorithm ignores edge weights and operates as a level-order algorithm
* on the number of hops. As such, it considers the path with the least number
* of hops to be shortest.
*
* This is particularly useful your Graph doesn't have Edge weights assigned to
* begin with or if you're merely interested in knowing which Vertices can be
* reached at all (path finding). This avoids running expensive operations to
* determine the actual weight (distance) of a path.
*/
class BreadthFirst extends Base
{
/**
Expand All @@ -36,6 +43,8 @@ public function getEdgesMap()
$vertexQueue = array();
$edges = array();

// $edges[$this->vertex->getId()] = array();

$vertexCurrent = $this->vertex;
$edgesCurrent = array();

Expand All @@ -62,15 +71,14 @@ public function getEdgesMap()

public function getEdgesTo(Vertex $endVertex)
{
if ($endVertex->getGraph() !== $this->vertex->getGraph()) {
throw new InvalidArgumentException('Given target vertex does not belong to the same graph instance');
}
$map = $this->getEdgesMap();
if (!isset($map[$endVertex->getId()])) {
throw new OutOfBoundsException('Given target vertex can not be reached from start vertex');
}
if ($endVertex->getGraph() === $this->vertex->getGraph()) {
$map = $this->getEdgesMap();

return $map[$endVertex->getId()];
if (isset($map[$endVertex->getId()])) {
return $map[$endVertex->getId()];
}
}
throw new OutOfBoundsException('Given target vertex can not be reached from start vertex');
}

/**
Expand All @@ -89,28 +97,11 @@ public function getDistanceMap()
return $ret;
}

/**
* checks whether there's a path from this start vertex to given end vertex
*
* @param Vertex $endVertex
* @return boolean
* @uses AlgorithmSpBreadthFirst::getEdgesMap()
*/
public function hasVertex(Vertex $endVertex)
{
if ($endVertex->getGraph() !== $this->vertex->getGraph()) {
throw new InvalidArgumentException('Given target vertex does not belong to the same graph instance');
}
$map = $this->getEdgesMap();

return isset($map[$endVertex->getId()]);
}

/**
* get array of all target vertices this vertex has a path to
*
* @return Vertex[]
* @uses AlgorithmSpBreadthFirst::getDistanceMap()
* @uses self::getEdgesMap()
*/
public function getVertices()
{
Expand Down
Loading

0 comments on commit be01ca5

Please sign in to comment.