From c6f36ed5d77059fa235390db9cedc928c43ef219 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Robles Date: Fri, 30 Aug 2013 14:15:58 -0500 Subject: [PATCH 1/4] First tests of a FloydWarshall Algorithm. --- .../Algorithm/ShortestPath/FloydWarshall.php | 162 +++++++++++ .../ShortestPath/FloydWarshallTest.php | 254 ++++++++++++++++++ 2 files changed, 416 insertions(+) create mode 100644 lib/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshall.php create mode 100644 tests/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshallTest.php diff --git a/lib/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshall.php b/lib/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshall.php new file mode 100644 index 00000000..70d1094f --- /dev/null +++ b/lib/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshall.php @@ -0,0 +1,162 @@ +vertex = $s; + } + + /** + * This method updates the cost and path arrays to create new entries if necessary + * @param $vertexSet array A container with the vertices of the graph. + * @param $pathCosts array A matrix with calculated costs between vertices. + * @param $cheapestPaths A matrix where each i, j entry holds an arrsy of Vertices + * @param $i int index from to update + * @param $j int index to to update + * + */ + protected function updateInfo(&$vertexSet, &$pathCosts, &$cheapestPaths, $i, $j) + { + + // If the index pair exists, return + if (array_key_exists($vertexSet[$i]->getId(), $cheapestPaths) && array_key_exists($vertexSet[$j]->getId(), + $cheapestPaths[$vertexSet[$i]->getId()]) + ) { + + return; + } + + // Creating new array indexes if necessary + if (!array_key_exists($vertexSet[$i]->getId(), $cheapestPaths)) { + + $cheapestPaths[$vertexSet[$i]->getId()] = array(); + } + + if (!array_key_exists($vertexSet[$j]->getId(), $cheapestPaths[$vertexSet[$i]->getId()])) { + + $cheapestPaths[$vertexSet[$i]->getId()][$vertexSet[$j]->getId()] = array(); + } + + if (!array_key_exists($i, $pathCosts)) { + + $pathCosts[$i] = array(); + } + + // If we have an edge between Vertices in positions i, j, we update the cost and asign the edge to the + // path between them, if not, the cost will be INF + $edge = $this->getEdgeFromTo($vertexSet[$i], $vertexSet[$j]); + + if ($edge) { + + $cheapestPaths[$vertexSet[$i]->getId()][$vertexSet[$j]->getId()] = array($edge); + $pathCosts[$i][$j] = $edge->getWeight(); + + } else { + + if ($i !== $j) { + + $pathCosts[$i][$j] = INF; + } + } + + } + + /** + * Auxiliary method for findind a directed edge between two vertices. + * @param Vertex $from + * @param Vertex $to + * @return \Fhaculty\Graph\Edge\Base|null The edge between $from and $to + */ + protected function getEdgeFromTo(Vertex $from, Vertex $to) + { + foreach ($from->getEdges() as $edge) { + if ($edge->isConnection($from, $to)) { + return $edge; + } + } + + return null; + } + + /** + * Get all edges on shortest path for every vertex in the graph where this vertex belongs. + * + * @return Edge[][][] For each i, j pair position of this array, an Edge list contains the shortest path from i to j + */ + public function getEdges() + { + $totalCostCheapestPathFromTo = array(); + $cheapestPathFromTo = array(); + $vertexSet = array_values($this->vertex->getGraph()->getVertices()); + + $nVertices = count($vertexSet); + + //Total cost from i to i is 0 + for ($i = 0; $i < $nVertices; ++$i) { + + $totalCostCheapestPathFromTo[$i][$i] = 0; + } + + // Calculating shortestPath(i,j,k+1) = min(shortestPath(i,j,k),shortestPath(i,k+1,k) + shortestPath(k+1,j,k)) + for ($k = 0; $k < $nVertices; ++$k) { + + for ($i = 0; $i < $nVertices; ++$i) { + + $this->updateInfo($vertexSet, $totalCostCheapestPathFromTo, $cheapestPathFromTo, $i, $k); + + for ($j = 0; $j < $nVertices; ++$j) { + + $this->updateInfo($vertexSet, $totalCostCheapestPathFromTo, $cheapestPathFromTo, $i, $k); + $this->updateInfo($vertexSet, $totalCostCheapestPathFromTo, $cheapestPathFromTo, $i, $j); + $this->updateInfo($vertexSet, $totalCostCheapestPathFromTo, $cheapestPathFromTo, $k, $j); + + // If we find that the path from (i, k) + (k, j) is shorter than the path (i, j), the new path is + // calculated. + if ($totalCostCheapestPathFromTo[$i][$k] + $totalCostCheapestPathFromTo[$k][$j] < $totalCostCheapestPathFromTo[$i][$j]) { + + $totalCostCheapestPathFromTo[$i][$j] = $totalCostCheapestPathFromTo[$i][$k] + $totalCostCheapestPathFromTo[$k][$j]; + + // Testing if we are not repeating a vertex' path over itself before merging the paths + if ($vertexSet[$i]->getId() != $vertexSet[$k]->getId() || $vertexSet[$k]->getId() != $vertexSet[$j]->getId()) { + + $cheapestPathFromTo[$vertexSet[$i]->getId()][$vertexSet[$j]->getId()] = array_merge($cheapestPathFromTo[$vertexSet[$i]->getId()][$vertexSet[$k]->getId()], + $cheapestPathFromTo[$vertexSet[$k]->getId()][$vertexSet[$j]->getId()]); + } + + } + + } + + } + } + + return $cheapestPathFromTo; + } + +} \ No newline at end of file diff --git a/tests/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshallTest.php b/tests/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshallTest.php new file mode 100644 index 00000000..bac700c8 --- /dev/null +++ b/tests/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshallTest.php @@ -0,0 +1,254 @@ +createVertex('A'); + + self::$graphs[1] = new Graph(); + $vertex[0] = self::$graphs[1]->createVertex('A'); + $vertex[1] = self::$graphs[1]->createVertex('B'); + $vertex[0]->createEdgeTo($vertex[1]); + $edges = $vertex[0]->getEdgesTo($vertex[1]); + $edges[0]->setWeight(1); + + self::$graphs[2] = new Graph(); + $vertex[2] = self::$graphs[2]->createVertex('A'); + $vertex[3] = self::$graphs[2]->createVertex('B'); + $vertex[4] = self::$graphs[2]->createVertex('C'); + $vertex[2]->createEdgeTo($vertex[3]); + $edges = $vertex[2]->getEdgesTo($vertex[3]); + $edges[0]->setWeight(1); + + $vertex[3]->createEdgeTo($vertex[4]); + $edges = $vertex[3]->getEdgesTo($vertex[4]); + $edges[0]->setWeight(2); + + self::$graphs[3] = new Graph(); + $vertex[5] = self::$graphs[3]->createVertex('A'); + $vertex[6] = self::$graphs[3]->createVertex('B'); + $vertex[7] = self::$graphs[3]->createVertex('C'); + $vertex[5]->createEdgeTo($vertex[6]); + $edges = $vertex[5]->getEdgesTo($vertex[6]); + $edges[0]->setWeight(1); + + $vertex[6]->createEdgeTo($vertex[7]); + $edges = $vertex[6]->getEdgesTo($vertex[7]); + $edges[0]->setWeight(1); + + $vertex[5]->createEdgeTo($vertex[7]); + $edges = $vertex[5]->getEdgesTo($vertex[7]); + $edges[0]->setWeight(3); + + // Will it work with negative weighted arcs??? + self::$graphs[4] = new Graph(); + $vertex[8] = self::$graphs[4]->createVertex('A'); + $vertex[8]->createEdgeTo($vertex[8]); + $edges = $vertex[8]->getEdgesTo($vertex[8]); + $edges[0]->setWeight(-1); + + self::$graphs[5] = new Graph(); + $vertex[9] = self::$graphs[5]->createVertex('1'); + $vertex[10] = self::$graphs[5]->createVertex('2'); + $vertex[11] = self::$graphs[5]->createVertex('3'); + $vertex[12] = self::$graphs[5]->createVertex('4'); + $vertex[9]->createEdgeTo($vertex[11]); + $edges = $vertex[9]->getEdgesTo($vertex[11]); + $edges[0]->setWeight(-2); + + $vertex[10]->createEdgeTo($vertex[11]); + $edges = $vertex[10]->getEdgesTo($vertex[11]); + $edges[0]->setWeight(3); + + $vertex[11]->createEdgeTo($vertex[12]); + $edges = $vertex[11]->getEdgesTo($vertex[12]); + $edges[0]->setWeight(2); + + $vertex[12]->createEdgeTo($vertex[10]); + $edges = $vertex[12]->getEdgesTo($vertex[10]); + $edges[0]->setWeight(-1); + + $vertex[10]->createEdgeTo($vertex[9]); + $edges = $vertex[10]->getEdgesTo($vertex[9]); + $edges[0]->setWeight(4); + + + } + + public function resultSizeProvider() + { + if(!self::$graphs) + self::setUpBeforeClass(); + + return array( + array( + self::$graphs[0]->getVertexFirst(), + 1, + array( + 'A' => 1 + ), + array( + 'A' => array( + 'A' => 0 + ) + ) + ), + array( + self::$graphs[1]->getVertexFirst(), + 2, + array( + 'A' => 2, + 'B' => 2 + ), + array( + 'A' => array( + 'A' => 0, + 'B' => 1 + ), + 'B' => array( + 'B' => 0 + ) + ) + ), + array( + self::$graphs[2]->getVertexFirst(), + 3, + array( + 'A' => 3, + 'B' => 3, + 'C' => 3 + ), + array( + 'A' => array( + 'A' => 0, + 'B' => 1, + 'C' => 2 + ), + 'B' => array( + + 'B' => 0, + 'C' => 1 + ), + 'C' => array( + 'C' => 0 + ) + ) + ), + array( + self::$graphs[3]->getVertexFirst(), + 3, + array( + 'A' => 3, + 'B' => 3, + 'C' => 3 + ), + array( + 'A' => array( + 'A' => 0, + 'B' => 1, + 'C' => 2 + ), + 'B' => array( + + 'B' => 0, + 'C' => 1 + ), + 'C' => array( + 'C' => 0 + ) + ) + ), + array( + self::$graphs[4]->getVertexFirst(), + 1, + array( + 'A' => 1 + ), + array( + 'A' => array( + 'A' => 1, + ) + ) + ), + array( + self::$graphs[5]->getVertexFirst(), + 4, + array( + '1' => 4, + '2' => 4, + '3' => 4, + '4' => 4 + ), + array( + '1' => array( + '1' => 0, + '2' => 3, + '3' => 1, + '4' => 2 + ), + '2' => array( + + '1' => 1, + '2' => 0, + '3' => 2, + '4' => 3 + ), + '3' => array( + '1' => 3, + '2' => 2, + '3' => 0, + '4' => 1 + ), + '4' => array( + '1' => 2, + '2' => 1, + '3' => 3, + '4' => 0 + ) + ) + ) + ); + } + + protected function createAlg(Vertex $vertex) + { + return new FloydWarshall($vertex); + } + + /** + * @dataProvider resultSizeProvider + */ + public function testExpectedSize($vertex, $nRows, $nColsByRow, $verticesByRowColumn) + { + $alg = $this->createAlg($vertex); + $shortestPaths = $alg->getEdges(); + + $t = count($shortestPaths); + $this->assertEquals($nRows, $t, 'The row size is incorrect.'); + + foreach ($nColsByRow as $key => $value) { + + $this->assertEquals($value, count($shortestPaths[$key]), 'The column size for row ' . $key . ' is incorrect.'); + } + + foreach ($verticesByRowColumn as $key => $row) { + + foreach ($row as $key2 => $val) { + + $this->assertEquals($val, count($shortestPaths[$key][$key2]), 'The vertices count for position ' . $key . ', ' . $key2 . ' is incorrect.'); + } + } + } +} \ No newline at end of file From 4c1a73ea6da2b3fe5ec7e96f174a033287cf2d61 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Robles Date: Thu, 3 Oct 2013 14:07:24 -0500 Subject: [PATCH 2/4] Refactored FloydWarshall to return a Result Interface, also it will throw an Exception in case of negative cycles (or loops), the Tests were also rewritten to be more specific. --- .gitignore | 1 + .../Algorithm/ShortestPath/FloydWarshall.php | 81 ++--- .../Graph/Algorithm/ShortestPath/Result.php | 119 ++++++++ .../ShortestPath/FloydWarshallTest.php | 287 ++++++++++-------- 4 files changed, 329 insertions(+), 159 deletions(-) create mode 100644 lib/Fhaculty/Graph/Algorithm/ShortestPath/Result.php diff --git a/.gitignore b/.gitignore index 4990c2dd..ce9cf95b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /vendor /composer.phar /build +/PHPUnitJetBrainsLoader.php \ No newline at end of file diff --git a/lib/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshall.php b/lib/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshall.php index 70d1094f..f39bfa96 100644 --- a/lib/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshall.php +++ b/lib/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshall.php @@ -2,36 +2,41 @@ namespace Fhaculty\Graph\Algorithm\ShortestPath; -use Fhaculty\Graph\Exception\InvalidArgumentException; +use Fhaculty\Graph\Exception\UnexpectedValueException; +use Fhaculty\Graph\Algorithm\BaseGraph; use Fhaculty\Graph\Vertex; /** * Class FloydWarshall * - * An implementation of the Floyd Warshall algorithm to find the shortest path from each vertex to each vertex. + * An implementation of the Floyd Warshall algorithm to find the all-pairs + * shortest path, with positive and negative weights. The original algorithm + * calculates the shortest distances between each pair of edges without giving + * information about the path itself, this implementation , however, returns + * a list of edges representing the shortest path for each pair [A, B], being + * A and B the origin and destination vertices respectively. + * + * If there is no path between two vertices A and B (a distance of INF between + * them), the path container for that pair will be empty. If A = B, then the + * the distance will be initially 0 (unless it has a positive loop, then it + * will initially take the weight of the loop)). + * + * This algorithm only works for directed and simple graphs, graphs with + * parallel edges will give wrong results. + * + * If the algorithm finds a negative cycle (including negative loops), it will + * throw an UnexpectedValueException. + * + * If the graph has no vertices, the algorithm will return an empty array, if + * has no edges, it will return an empty nxn array where n is the number of + * vertices. + * * @link http://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm * - * @package Fhaculty\Graph\Algorithm\ShortestPath */ -class FloydWarshall +class FloydWarshall extends BaseGraph { - /** - * @var Vertex - */ - protected $vertex; - - public function __construct($s) - { - - if (!(get_class($s) === 'Fhaculty\Graph\Vertex')) { - - throw new InvalidArgumentException('This algorithm only receives Vertex objects.'); - } - - $this->vertex = $s; - } - /** * This method updates the cost and path arrays to create new entries if necessary * @param $vertexSet array A container with the vertices of the graph. @@ -39,6 +44,7 @@ public function __construct($s) * @param $cheapestPaths A matrix where each i, j entry holds an arrsy of Vertices * @param $i int index from to update * @param $j int index to to update + * @throws UnexpectedValueException when encountering a negative cycle * */ protected function updateInfo(&$vertexSet, &$pathCosts, &$cheapestPaths, $i, $j) @@ -68,9 +74,9 @@ protected function updateInfo(&$vertexSet, &$pathCosts, &$cheapestPaths, $i, $j) $pathCosts[$i] = array(); } - // If we have an edge between Vertices in positions i, j, we update the cost and asign the edge to the + // If we have an edge between Vertices in positions i, j, we update the cost and assign the edge to the // path between them, if not, the cost will be INF - $edge = $this->getEdgeFromTo($vertexSet[$i], $vertexSet[$j]); + $edge = $this->getEdgeBetween($vertexSet[$i], $vertexSet[$j]); if ($edge) { @@ -88,32 +94,32 @@ protected function updateInfo(&$vertexSet, &$pathCosts, &$cheapestPaths, $i, $j) } /** - * Auxiliary method for findind a directed edge between two vertices. + * Auxiliary method for finding a directed edge between two vertices. * @param Vertex $from * @param Vertex $to * @return \Fhaculty\Graph\Edge\Base|null The edge between $from and $to */ - protected function getEdgeFromTo(Vertex $from, Vertex $to) + protected function getEdgeBetween(Vertex $from, Vertex $to) { - foreach ($from->getEdges() as $edge) { - if ($edge->isConnection($from, $to)) { - return $edge; - } - } - return null; + $edges = $from->getEdgesTo($to); + return count($edges) > 0 ? $edges[0] : null; } /** - * Get all edges on shortest path for every vertex in the graph where this vertex belongs. + * Get all edges on shortest path for every vertex in the graph where this + * vertex belongs. * - * @return Edge[][][] For each i, j pair position of this array, an Edge list contains the shortest path from i to j + * @return Result A Result object with the interface for handling the + * generated edge table list contains the shortest path from i to j + * @throws UnexpectedValueException when encountering a cycle with + * negative weight. */ - public function getEdges() + public function createResult() { $totalCostCheapestPathFromTo = array(); $cheapestPathFromTo = array(); - $vertexSet = array_values($this->vertex->getGraph()->getVertices()); + $vertexSet = array_values($this->graph->getVertices()); $nVertices = count($vertexSet); @@ -154,9 +160,14 @@ public function getEdges() } } + + if($totalCostCheapestPathFromTo[$k][$k] < 0) { + + throw new UnexpectedValueException('Floyd-Warshall not supported for negative cycles'); + } } - return $cheapestPathFromTo; + return new Result($cheapestPathFromTo, $this->graph); } } \ No newline at end of file diff --git a/lib/Fhaculty/Graph/Algorithm/ShortestPath/Result.php b/lib/Fhaculty/Graph/Algorithm/ShortestPath/Result.php new file mode 100644 index 00000000..58806141 --- /dev/null +++ b/lib/Fhaculty/Graph/Algorithm/ShortestPath/Result.php @@ -0,0 +1,119 @@ +edgeTable = $edgeTable; + $this->graph = $graph; + } + + /** + * Get a list of edges common to every pair's shortest path. + * + * @return Edge[] + */ + public function getEdges() + { + + return $this->createGraph()->getEdges(); + } + + /** + * Creates a new Graph based on the provided graph, removing the edges not + * present in any shortest path. + * + * @return Graph The new graph with the edges in each pair's shortest path. + */ + public function createGraph() + { + $newGraph = $this->graph->createGraphCloneEdgeless(); + + // Copying edge from edge from the table to the new graph when needed. + foreach ($this->edgeTable as $idx => $vertexRow) { + + foreach ($vertexRow as $idx2 => $vertexCol) { + + foreach ($vertexCol as $edge) { + + if (!($newGraph->getVertex($edge->getVertexStart()->getId())->hasEdgeTo($newGraph->getVertex($edge->getVertexEnd()->getId())))) { + + $newGraph->createEdgeClone($edge); + } + } + } + } + + return $newGraph; + } + + /** + * Returns the original Graph provided to this result. + * + * @return Graph + */ + public function getGraph() + { + return $this->graph; + } + + /** + * Returns an array of paths (Walks), each key of this array is the id of a + * vertex in the Graph and it references the path (Walk) with the Vertex + * associated to that Id as the start Vertex for that path. + + + * @return Walk[] + */ + public function getPaths() + { + $paths = array(); + + foreach ($this->edgeTable as $idx => $vertexRow) { + + foreach ($vertexRow as $idx2 => $vertexCol) { + + $paths[$idx] = Walk::factoryFromEdges($vertexCol, $this->graph->getVertex($idx)); + } + } + + return $paths; + } + + /** + * Returns the original Edge table provided for this result. + * + * @return Edge[][][] + */ + public function getEdgeTable() + { + return $this->edgeTable; + } + +} \ No newline at end of file diff --git a/tests/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshallTest.php b/tests/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshallTest.php index bac700c8..570a93d9 100644 --- a/tests/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshallTest.php +++ b/tests/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshallTest.php @@ -46,7 +46,7 @@ public static function setUpBeforeClass() $vertex[6]->createEdgeTo($vertex[7]); $edges = $vertex[6]->getEdgesTo($vertex[7]); - $edges[0]->setWeight(1); + $edges[0]->setWeight(3); $vertex[5]->createEdgeTo($vertex[7]); $edges = $vertex[5]->getEdgesTo($vertex[7]); @@ -84,171 +84,210 @@ public static function setUpBeforeClass() $edges = $vertex[10]->getEdgesTo($vertex[9]); $edges[0]->setWeight(4); + // And with a negative cycle?? + self::$graphs[6] = new Graph(); + $vertex[13] = self::$graphs[6]->createVertex('1'); + $vertex[14] = self::$graphs[6]->createVertex('2'); + $vertex[15] = self::$graphs[6]->createVertex('3'); + + $vertex[13]->createEdgeTo($vertex[14]); + $edges = $vertex[13]->getEdgesTo($vertex[14]); + $edges[0]->setWeight(-1); + + $vertex[14]->createEdgeTo($vertex[15]); + $edges = $vertex[14]->getEdgesTo($vertex[15]); + $edges[0]->setWeight(-1); + + $vertex[15]->createEdgeTo($vertex[13]); + $edges = $vertex[15]->getEdgesTo($vertex[13]); + $edges[0]->setWeight(-1); + + // Empty Graphs + self::$graphs[7] = new Graph(); + + // Isolated Vertices graphs + self::$graphs[8] = new Graph(); + $vertex[16] = self::$graphs[8]->createVertex('1'); + $vertex[17] = self::$graphs[8]->createVertex('2'); + $vertex[18] = self::$graphs[8]->createVertex('3'); + + self::$graphs[9] = new Graph(); + + $vertex[19] = self::$graphs[9]->createVertex('1'); + $vertex[20] = self::$graphs[9]->createVertex('2'); + $vertex[21] = self::$graphs[9]->createVertex('3'); + $vertex[22] = self::$graphs[9]->createVertex('4'); + + $vertex[20]->createEdgeTo($vertex[21]); + $edges = $vertex[20]->getEdgesTo($vertex[21]); + $edges[0]->setWeight(1); + + $vertex[21]->createEdgeTo($vertex[22]); + $edges = $vertex[21]->getEdgesTo($vertex[22]); + $edges[0]->setWeight(2); + + $vertex[22]->createEdgeTo($vertex[21]); + $edges = $vertex[22]->getEdgesTo($vertex[21]); + $edges[0]->setWeight(1); } - public function resultSizeProvider() + /** + * Provider for test graphs and expected sequences of nodes for each resulting path(walk) + * @return array + */ + public function graphPathsProvider() { if(!self::$graphs) self::setUpBeforeClass(); return array( array( - self::$graphs[0]->getVertexFirst(), - 1, - array( - 'A' => 1 - ), - array( - 'A' => array( - 'A' => 0 - ) - ) + self::$graphs[0], + array('A' => array('A')) ), array( - self::$graphs[1]->getVertexFirst(), - 2, - array( - 'A' => 2, - 'B' => 2 - ), + self::$graphs[1], array( - 'A' => array( - 'A' => 0, - 'B' => 1 - ), - 'B' => array( - 'B' => 0 - ) + 'A' => array('A', 'B'), + 'B' => array('B') ) ), array( - self::$graphs[2]->getVertexFirst(), - 3, + self::$graphs[2], array( - 'A' => 3, - 'B' => 3, - 'C' => 3 - ), - array( - 'A' => array( - 'A' => 0, - 'B' => 1, - 'C' => 2 - ), - 'B' => array( - - 'B' => 0, - 'C' => 1 - ), - 'C' => array( - 'C' => 0 - ) + 'A' => array('A', 'B', 'C'), + 'B' => array('B', 'C'), + 'C' => array('C') ) ), array( - self::$graphs[3]->getVertexFirst(), - 3, - array( - 'A' => 3, - 'B' => 3, - 'C' => 3 - ), + self::$graphs[3], array( - 'A' => array( - 'A' => 0, - 'B' => 1, - 'C' => 2 - ), - 'B' => array( - - 'B' => 0, - 'C' => 1 - ), - 'C' => array( - 'C' => 0 - ) + 'A' => array('A', 'C'), + 'B' => array('B', 'C'), + 'C' => array('C') ) ), array( - self::$graphs[4]->getVertexFirst(), - 1, - array( - 'A' => 1 - ), + self::$graphs[5], array( - 'A' => array( - 'A' => 1, - ) - ) - ), - array( - self::$graphs[5]->getVertexFirst(), - 4, - array( - '1' => 4, - '2' => 4, - '3' => 4, - '4' => 4 - ), - array( - '1' => array( - '1' => 0, - '2' => 3, - '3' => 1, - '4' => 2 - ), - '2' => array( - - '1' => 1, - '2' => 0, - '3' => 2, - '4' => 3 - ), - '3' => array( - '1' => 3, - '2' => 2, - '3' => 0, - '4' => 1 - ), - '4' => array( - '1' => 2, - '2' => 1, - '3' => 3, - '4' => 0 - ) + '1' => array('1', '3', '4'), + '2' => array('2', '1', '3', '4'), + '3' => array('3', '4'), + '4' => array('4') ) ) ); } - protected function createAlg(Vertex $vertex) + /** + * Provider for empty graph tests. + * @return array + */ + public function emptyGraphProvider() + { + if(!self::$graphs) + self::setUpBeforeClass(); + + return array( + array(self::$graphs[7]) + ); + } + + /** + * Provider for negative cycle graph tests. + * @return array + */ + public function negativeCycleGraphProvider() + { + if(!self::$graphs) + self::setUpBeforeClass(); + + return array( + array(self::$graphs[6]), + array(self::$graphs[4]) + ); + } + + /** + * Provider for isolated vertices graph tests. + * @return array + */ + public function isolatedVerticesGraphProvider() + { + if(!self::$graphs) + self::setUpBeforeClass(); + + return array( + array(self::$graphs[8], 0), + array(self::$graphs[9], 3) + ); + } + + protected function createAlg(Graph $graph) { - return new FloydWarshall($vertex); + return new FloydWarshall($graph); } /** - * @dataProvider resultSizeProvider + * @dataProvider emptyGraphProvider */ - public function testExpectedSize($vertex, $nRows, $nColsByRow, $verticesByRowColumn) + public function testEmptyGraph($testGraph) { - $alg = $this->createAlg($vertex); - $shortestPaths = $alg->getEdges(); + $alg = $this->createAlg($testGraph); + $result = $alg->createResult()->createGraph(); + $this->assertTrue($result->isEmpty(), 'The graph generated by Floyd Warshall over an empty graph must be an empty graph'); + } - $t = count($shortestPaths); - $this->assertEquals($nRows, $t, 'The row size is incorrect.'); + /** + * @dataProvider isolatedVerticesGraphProvider + */ + public function testIsolatedVertices($testGraph, $expectedEdgeNumber) + { - foreach ($nColsByRow as $key => $value) { + $alg = $this->createAlg($testGraph); + $result = $alg->createResult(); + $result = $result->createGraph(); + $this->assertEquals($testGraph->getNumberOfVertices(), $result->getNumberOfVertices(), 'The number of vertices must be equal.'); + $this->assertEquals($expectedEdgeNumber, $result->getNumberOfEdges(), 'The number of edges must be ' . $expectedEdgeNumber); + } - $this->assertEquals($value, count($shortestPaths[$key]), 'The column size for row ' . $key . ' is incorrect.'); + /** + * Testing resulting Paths for a Floyd Warshall algorithm + * + * @dataProvider graphPathsProvider + */ + public function testResultingPath($testGraph, $expectedNodePairSequence) + { + $alg = $this->createAlg($testGraph); + $result = $alg->createResult()->getPaths(); + + foreach ($result as $idxNode => $path) { + + $seq = $path->getVerticesSequenceId(); + $this->assertTrue($expectedNodePairSequence[$idxNode] == $seq); } - foreach ($verticesByRowColumn as $key => $row) { + } - foreach ($row as $key2 => $val) { + /** + * Testing the Exception in case of a negative cycle. + * + * @dataProvider negativeCycleGraphProvider + */ + public function testNegativeCycles($testGraph) + { + + try { - $this->assertEquals($val, count($shortestPaths[$key][$key2]), 'The vertices count for position ' . $key . ', ' . $key2 . ' is incorrect.'); - } + $alg = $this->createAlg($testGraph); + $alg->createResult(); + } catch (UnexpectedValueException $uve) { + + return; } + + $this->fail('An expected UnexpectedValueException has not been raised.'); } } \ No newline at end of file From 0e3156752767175c2c0f4e93230df9116f93b5eb Mon Sep 17 00:00:00 2001 From: Juan Sebastian Robles Jimenez Date: Thu, 3 Oct 2013 14:45:00 -0500 Subject: [PATCH 3/4] Update .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index ce9cf95b..4990c2dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ /vendor /composer.phar /build -/PHPUnitJetBrainsLoader.php \ No newline at end of file From c87f87a0e58976d82fd7a47352986a3d02893eea Mon Sep 17 00:00:00 2001 From: Juan Sebastian Robles Date: Mon, 18 Nov 2013 16:27:24 -0500 Subject: [PATCH 4/4] Some minor fixes on the Floyd Warshall test, the Floyd Warshall algorithm classes moved to new namespace, and Result class had the createGraph and getVertices methods rewritten. --- .../{ => AllPairs}/FloydWarshall.php | 6 +- .../FloydWarshallResult.php} | 47 ++++---- .../ShortestPath/FloydWarshallTest.php | 102 ++++-------------- 3 files changed, 49 insertions(+), 106 deletions(-) rename lib/Fhaculty/Graph/Algorithm/ShortestPath/{ => AllPairs}/FloydWarshall.php (96%) rename lib/Fhaculty/Graph/Algorithm/ShortestPath/{Result.php => AllPairs/FloydWarshallResult.php} (68%) diff --git a/lib/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshall.php b/lib/Fhaculty/Graph/Algorithm/ShortestPath/AllPairs/FloydWarshall.php similarity index 96% rename from lib/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshall.php rename to lib/Fhaculty/Graph/Algorithm/ShortestPath/AllPairs/FloydWarshall.php index f39bfa96..a256abf6 100644 --- a/lib/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshall.php +++ b/lib/Fhaculty/Graph/Algorithm/ShortestPath/AllPairs/FloydWarshall.php @@ -1,6 +1,6 @@ graph); + return new FloydWarshallResult($cheapestPathFromTo, $this->graph); } } \ No newline at end of file diff --git a/lib/Fhaculty/Graph/Algorithm/ShortestPath/Result.php b/lib/Fhaculty/Graph/Algorithm/ShortestPath/AllPairs/FloydWarshallResult.php similarity index 68% rename from lib/Fhaculty/Graph/Algorithm/ShortestPath/Result.php rename to lib/Fhaculty/Graph/Algorithm/ShortestPath/AllPairs/FloydWarshallResult.php index 58806141..47eb12b9 100644 --- a/lib/Fhaculty/Graph/Algorithm/ShortestPath/Result.php +++ b/lib/Fhaculty/Graph/Algorithm/ShortestPath/AllPairs/FloydWarshallResult.php @@ -1,21 +1,22 @@ createGraph()->getEdges(); - } - - /** - * Creates a new Graph based on the provided graph, removing the edges not - * present in any shortest path. - * - * @return Graph The new graph with the edges in each pair's shortest path. - */ - public function createGraph() - { - $newGraph = $this->graph->createGraphCloneEdgeless(); + $foundEdges = array(); - // Copying edge from edge from the table to the new graph when needed. foreach ($this->edgeTable as $idx => $vertexRow) { foreach ($vertexRow as $idx2 => $vertexCol) { foreach ($vertexCol as $edge) { - if (!($newGraph->getVertex($edge->getVertexStart()->getId())->hasEdgeTo($newGraph->getVertex($edge->getVertexEnd()->getId())))) { + if (!in_array($edge, $foundEdges, true)) { - $newGraph->createEdgeClone($edge); + $foundEdges[] = $edge; } } } } - return $newGraph; + return $foundEdges; + } + + /** + * Creates a new Graph based on the provided graph, without the edges not + * present in any shortest path. + * + * @return Graph The new graph with the edges in each pair's shortest path. + */ + public function createGraph() + { + + return $this->graph->createGraphCloneEdges($this->getEdges()); } /** diff --git a/tests/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshallTest.php b/tests/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshallTest.php index 570a93d9..4f947d00 100644 --- a/tests/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshallTest.php +++ b/tests/Fhaculty/Graph/Algorithm/ShortestPath/FloydWarshallTest.php @@ -1,8 +1,8 @@ createVertex('A'); $vertex[1] = self::$graphs[1]->createVertex('B'); - $vertex[0]->createEdgeTo($vertex[1]); - $edges = $vertex[0]->getEdgesTo($vertex[1]); - $edges[0]->setWeight(1); + $vertex[0]->createEdgeTo($vertex[1])->setWeight(1); self::$graphs[2] = new Graph(); $vertex[2] = self::$graphs[2]->createVertex('A'); $vertex[3] = self::$graphs[2]->createVertex('B'); $vertex[4] = self::$graphs[2]->createVertex('C'); - $vertex[2]->createEdgeTo($vertex[3]); - $edges = $vertex[2]->getEdgesTo($vertex[3]); - $edges[0]->setWeight(1); - - $vertex[3]->createEdgeTo($vertex[4]); - $edges = $vertex[3]->getEdgesTo($vertex[4]); - $edges[0]->setWeight(2); + $vertex[2]->createEdgeTo($vertex[3])->setWeight(1); + $vertex[3]->createEdgeTo($vertex[4])->setWeight(2); self::$graphs[3] = new Graph(); $vertex[5] = self::$graphs[3]->createVertex('A'); $vertex[6] = self::$graphs[3]->createVertex('B'); $vertex[7] = self::$graphs[3]->createVertex('C'); - $vertex[5]->createEdgeTo($vertex[6]); - $edges = $vertex[5]->getEdgesTo($vertex[6]); - $edges[0]->setWeight(1); - - $vertex[6]->createEdgeTo($vertex[7]); - $edges = $vertex[6]->getEdgesTo($vertex[7]); - $edges[0]->setWeight(3); - - $vertex[5]->createEdgeTo($vertex[7]); - $edges = $vertex[5]->getEdgesTo($vertex[7]); - $edges[0]->setWeight(3); + $vertex[5]->createEdgeTo($vertex[6])->setWeight(1); + $vertex[6]->createEdgeTo($vertex[7])->setWeight(3); + $vertex[5]->createEdgeTo($vertex[7])->setWeight(3); // Will it work with negative weighted arcs??? self::$graphs[4] = new Graph(); $vertex[8] = self::$graphs[4]->createVertex('A'); - $vertex[8]->createEdgeTo($vertex[8]); - $edges = $vertex[8]->getEdgesTo($vertex[8]); - $edges[0]->setWeight(-1); + $vertex[8]->createEdgeTo($vertex[8])->setWeight(-1); self::$graphs[5] = new Graph(); $vertex[9] = self::$graphs[5]->createVertex('1'); $vertex[10] = self::$graphs[5]->createVertex('2'); $vertex[11] = self::$graphs[5]->createVertex('3'); $vertex[12] = self::$graphs[5]->createVertex('4'); - $vertex[9]->createEdgeTo($vertex[11]); - $edges = $vertex[9]->getEdgesTo($vertex[11]); - $edges[0]->setWeight(-2); - - $vertex[10]->createEdgeTo($vertex[11]); - $edges = $vertex[10]->getEdgesTo($vertex[11]); - $edges[0]->setWeight(3); - - $vertex[11]->createEdgeTo($vertex[12]); - $edges = $vertex[11]->getEdgesTo($vertex[12]); - $edges[0]->setWeight(2); - - $vertex[12]->createEdgeTo($vertex[10]); - $edges = $vertex[12]->getEdgesTo($vertex[10]); - $edges[0]->setWeight(-1); - - $vertex[10]->createEdgeTo($vertex[9]); - $edges = $vertex[10]->getEdgesTo($vertex[9]); - $edges[0]->setWeight(4); + $vertex[9]->createEdgeTo($vertex[11])->setWeight(-2); + $vertex[10]->createEdgeTo($vertex[11])->setWeight(3); + $vertex[11]->createEdgeTo($vertex[12])->setWeight(2); + $vertex[12]->createEdgeTo($vertex[10])->setWeight(-1); + $vertex[10]->createEdgeTo($vertex[9])->setWeight(4); // And with a negative cycle?? self::$graphs[6] = new Graph(); $vertex[13] = self::$graphs[6]->createVertex('1'); $vertex[14] = self::$graphs[6]->createVertex('2'); $vertex[15] = self::$graphs[6]->createVertex('3'); - - $vertex[13]->createEdgeTo($vertex[14]); - $edges = $vertex[13]->getEdgesTo($vertex[14]); - $edges[0]->setWeight(-1); - - $vertex[14]->createEdgeTo($vertex[15]); - $edges = $vertex[14]->getEdgesTo($vertex[15]); - $edges[0]->setWeight(-1); - - $vertex[15]->createEdgeTo($vertex[13]); - $edges = $vertex[15]->getEdgesTo($vertex[13]); - $edges[0]->setWeight(-1); + $vertex[13]->createEdgeTo($vertex[14])->setWeight(-1); + $vertex[14]->createEdgeTo($vertex[15])->setWeight(-1); + $vertex[15]->createEdgeTo($vertex[13])->setWeight(-1); // Empty Graphs self::$graphs[7] = new Graph(); @@ -113,23 +73,13 @@ public static function setUpBeforeClass() $vertex[18] = self::$graphs[8]->createVertex('3'); self::$graphs[9] = new Graph(); - $vertex[19] = self::$graphs[9]->createVertex('1'); $vertex[20] = self::$graphs[9]->createVertex('2'); $vertex[21] = self::$graphs[9]->createVertex('3'); $vertex[22] = self::$graphs[9]->createVertex('4'); - - $vertex[20]->createEdgeTo($vertex[21]); - $edges = $vertex[20]->getEdgesTo($vertex[21]); - $edges[0]->setWeight(1); - - $vertex[21]->createEdgeTo($vertex[22]); - $edges = $vertex[21]->getEdgesTo($vertex[22]); - $edges[0]->setWeight(2); - - $vertex[22]->createEdgeTo($vertex[21]); - $edges = $vertex[22]->getEdgesTo($vertex[21]); - $edges[0]->setWeight(1); + $vertex[20]->createEdgeTo($vertex[21])->setWeight(1); + $vertex[21]->createEdgeTo($vertex[22])->setWeight(2); + $vertex[22]->createEdgeTo($vertex[21])->setWeight(1); } /** @@ -275,19 +225,11 @@ public function testResultingPath($testGraph, $expectedNodePairSequence) * Testing the Exception in case of a negative cycle. * * @dataProvider negativeCycleGraphProvider + * @expectedException UnexpectedValueException */ public function testNegativeCycles($testGraph) { - - try { - - $alg = $this->createAlg($testGraph); - $alg->createResult(); - } catch (UnexpectedValueException $uve) { - - return; - } - - $this->fail('An expected UnexpectedValueException has not been raised.'); + $alg = $this->createAlg($testGraph); + $alg->createResult(); } } \ No newline at end of file