216 lines
5.7 KiB
PHP
216 lines
5.7 KiB
PHP
|
<?php
|
||
|
|
||
|
namespace Graphp\Algorithms\TravelingSalesmanProblem;
|
||
|
|
||
|
use Fhaculty\Graph\Exception\UnexpectedValueException;
|
||
|
|
||
|
use Fhaculty\Graph\Exception\UnderflowException;
|
||
|
|
||
|
use Fhaculty\Graph\Graph;
|
||
|
use Fhaculty\Graph\Vertex;
|
||
|
use Fhaculty\Graph\Set\Edges;
|
||
|
use Graphp\Algorithms\TravelingSalesmanProblem\MinimumSpanningTree as AlgorithmTspMst;
|
||
|
|
||
|
class Bruteforce extends Base4
|
||
|
{
|
||
|
/**
|
||
|
*
|
||
|
* @var Graph
|
||
|
*/
|
||
|
private $graph;
|
||
|
|
||
|
/**
|
||
|
* best weight so for (used for branch-and-bound)
|
||
|
*
|
||
|
* @var number|NULL
|
||
|
*/
|
||
|
private $bestWeight;
|
||
|
|
||
|
/**
|
||
|
* reference to start vertex
|
||
|
*
|
||
|
* @var Vertex
|
||
|
*/
|
||
|
private $startVertex;
|
||
|
|
||
|
/**
|
||
|
* total number of edges needed
|
||
|
*
|
||
|
* @var int
|
||
|
*/
|
||
|
private $numEdges;
|
||
|
|
||
|
/**
|
||
|
* upper limit to use for branch-and-bound (BNB)
|
||
|
*
|
||
|
* @var float|NULL
|
||
|
* @see AlgorithmTspBruteforce::setUpperLimit()
|
||
|
*/
|
||
|
private $upperLimit = NULL;
|
||
|
|
||
|
/**
|
||
|
* whether to use branch-and-bound
|
||
|
*
|
||
|
* simply put, there's no valid reason why anybody would want to turn off this flag
|
||
|
*
|
||
|
* @var boolean
|
||
|
*/
|
||
|
private $branchAndBound = true;
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* @param Graph $graph
|
||
|
*/
|
||
|
public function __construct(Graph $graph)
|
||
|
{
|
||
|
$this->graph = $graph;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* explicitly set upper limit to use for branch-and-bound
|
||
|
*
|
||
|
* this method can be used to optimize the algorithm by providing an upper
|
||
|
* bound of when to stop branching any further.
|
||
|
*
|
||
|
* @param double $limit
|
||
|
* @return AlgorithmTspBruteforce $this (chainable)
|
||
|
*/
|
||
|
public function setUpperLimit($limit)
|
||
|
{
|
||
|
$this->upperLimit = $limit;
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
public function setUpperLimitMst()
|
||
|
{
|
||
|
$alg = new AlgorithmTspMst($this->graph);
|
||
|
$limit = $alg->createGraph()->getWeight();
|
||
|
|
||
|
return $this->setUpperLimit($limit);
|
||
|
}
|
||
|
|
||
|
protected function getVertexStart()
|
||
|
{
|
||
|
// actual start doesn't really matter as we're only considering complete graphs here
|
||
|
return $this->graph->getVertices()->getVertexFirst();
|
||
|
}
|
||
|
|
||
|
protected function getGraph()
|
||
|
{
|
||
|
return $this->graph;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* get resulting (first) best circle of edges connecting all vertices
|
||
|
*
|
||
|
* @throws Exception on error
|
||
|
* @return Edges
|
||
|
*/
|
||
|
public function getEdges()
|
||
|
{
|
||
|
$this->numEdges = count($this->graph->getVertices());
|
||
|
if ($this->numEdges < 3) {
|
||
|
throw new UnderflowException('Needs at least 3 vertices');
|
||
|
}
|
||
|
|
||
|
// numEdges 3-12 should work
|
||
|
|
||
|
$this->bestWeight = $this->upperLimit;
|
||
|
$this->startVertex = $this->getVertexStart();
|
||
|
|
||
|
$result = $this->step($this->startVertex,
|
||
|
0,
|
||
|
array(),
|
||
|
array()
|
||
|
);
|
||
|
|
||
|
if ($result === NULL) {
|
||
|
throw new UnexpectedValueException('No resulting solution for TSP found');
|
||
|
}
|
||
|
|
||
|
return new Edges($result);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* @param Vertex $vertex current point-of-view
|
||
|
* @param number $totalWeight total weight (so far)
|
||
|
* @param boolean[] $visitedVertices
|
||
|
* @param Edge[] $visitedEdges
|
||
|
* @return Edge[]
|
||
|
*/
|
||
|
private function step(Vertex $vertex, $totalWeight, array $visitedVertices, array $visitedEdges)
|
||
|
{
|
||
|
// stop recursion if best result is exceeded (branch and bound)
|
||
|
if ($this->branchAndBound && $this->bestWeight !== NULL && $totalWeight >= $this->bestWeight) {
|
||
|
return NULL;
|
||
|
}
|
||
|
// kreis geschlossen am Ende
|
||
|
if ($vertex === $this->startVertex && count($visitedEdges) === $this->numEdges) {
|
||
|
// new best result
|
||
|
$this->bestWeight = $totalWeight;
|
||
|
|
||
|
return $visitedEdges;
|
||
|
}
|
||
|
|
||
|
// only visit each vertex once
|
||
|
if (isset($visitedVertices[$vertex->getId()])) {
|
||
|
return NULL;
|
||
|
}
|
||
|
$visitedVertices[$vertex->getId()] = true;
|
||
|
|
||
|
$bestResult = NULL;
|
||
|
|
||
|
// weiter verzweigen in alle vertices
|
||
|
foreach ($vertex->getEdgesOut() as $edge) {
|
||
|
// get target vertex of this edge
|
||
|
$target = $edge->getVertexToFrom($vertex);
|
||
|
|
||
|
$weight = $edge->getWeight();
|
||
|
if ($weight < 0) {
|
||
|
throw new UnexpectedValueException('Edge with negative weight "' . $weight . '" not supported');
|
||
|
}
|
||
|
|
||
|
$result = $this->step($target,
|
||
|
$totalWeight + $weight,
|
||
|
$visitedVertices,
|
||
|
array_merge($visitedEdges, array($edge))
|
||
|
);
|
||
|
|
||
|
// new result found
|
||
|
if ($result !== NULL) {
|
||
|
// branch and bound enabled (default): returned result MUST be the new best result
|
||
|
if($this->branchAndBound ||
|
||
|
// this is the first result, just use it anyway
|
||
|
$bestResult === NULL ||
|
||
|
// this is the new best result
|
||
|
$this->sumEdges($result) < $this->sumEdges($bestResult)){
|
||
|
$bestResult = $result;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $bestResult;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* get sum of weight of given edges
|
||
|
*
|
||
|
* no need to optimize this further, as it's only evaluated if branchAndBound is disabled and
|
||
|
* there's no valid reason why anybody would want to do so.
|
||
|
*
|
||
|
* @param Edge[] $edges
|
||
|
* @return float
|
||
|
*/
|
||
|
private function sumEdges(array $edges)
|
||
|
{
|
||
|
$sum = 0;
|
||
|
foreach ($edges as $edge) {
|
||
|
$sum += $edge->getWeight();
|
||
|
}
|
||
|
|
||
|
return $sum;
|
||
|
}
|
||
|
}
|