/Heuristics.php
PHP | 598 lines | 453 code | 98 blank | 47 comment | 66 complexity | 75d5705fab3d1ecfd98d9bbd2b6efd1e MD5 | raw file
- <?php
- namespace MrCeperka\MIPAA\Heuristics;
- require_once 'Helpers.php';
- use MrCeperka\MIPAA\Helpers\Node;
- use MrCeperka\MIPAA\Helpers\PriceWeightRatioHelper;
- interface IHeuristics
- {
- public function execute();
-
- public function getResult();
-
- public function logResult();
- }
- abstract class AHeuristicsTemplate implements IHeuristics
- {
- protected $result = [];
- protected $input;
-
-
- public function __construct($input)
- {
- $this->input = $input;
- }
-
- public final function execute()
- {
- $start = microtime(true);
- $pairs = $this->beforeCompute();
- $r = $this->compute($pairs);
- $end = microtime(true) - $start;
- $end *= 1000 * 1000;
-
- $this->result = array_merge(
- [
- 'ID' => $this->input['ID'],
- 'n' => $this->input['n'],
- 'M' => $this->input['M'],
- 'time' => round($end),
- ],
- $r
- );
- }
-
- public function getResult()
- {
- return $this->result;
- }
-
- protected abstract function beforeCompute();
-
- protected function compute($pairs)
- {
- $sumPrice = 0;
- $sumWeight = 0;
- $items = [];
-
- for ($i = 0; $i < $this->input['n']; $i++) {
- if ($sumWeight + $pairs[$i]['weight'] <= $this->input['M']) {
- $sumPrice += $pairs[$i]['price'];
- $sumWeight += $pairs[$i]['weight'];
- $items[] = $pairs[$i];
- } else {
- //try to find another item with the biggest price
- for ($j = $i; $j < $this->input['n']; $j++) {
- if ($sumWeight + $pairs[$i]['weight'] <= $this->input['M']) {
- $sumPrice += $pairs[$i]['price'];
- $sumWeight += $pairs[$i]['weight'];
- $items[] = $pairs[$i];
- }
- }
- break;
- }
- }
- return [
- 'sumPrice' => $sumPrice,
- 'sumWeight' => $sumWeight,
- 'items' => $items,
- ];
- }
-
- public function logResult()
- {
- $logString = '-------------------------------' . "\n";
- $logString .= implode(' ', [$this->result['ID'], $this->result['n'], $this->result['M']]) . "\n";
- $logString .= $this->result['ID'] . ' sumPrice: ' . $this->result['sumPrice'] . ' sumWeight: ' . $this->result['sumWeight'] . "\n";
-
- //generate map of items in bag
- $resultHashed = [];
- $outcome = [];
- foreach ($this->result['items'] as $pair) {
- $resultHashed[$this->formatKey($pair)] = $pair;
- }
-
- foreach ($this->input['pairsGen']() as $pair) {
- if (!isset($resultHashed[$this->formatKey($pair)])) {
- $outcome[$this->formatKey($pair)] = '0';
- } else {
- $outcome[$this->formatKey($pair)] = '1';
- }
- }
-
- $logString .= 'itemsMap: ' . "\n";
- foreach ($outcome as $key => $value) {
- $logString .= $key . ' => ' . $value . "\n";
- }
- $logString .= '-------------------------------' . "\n";
-
- file_put_contents(OUTPUT_DIR . strtolower((new \ReflectionClass($this))->getShortName()) . '.log', $logString, FILE_APPEND);
- }
-
- protected function formatKey($pair)
- {
- return $pair['price'] . ' : ' . $pair['weight'];
- }
- }
- class MaxPriceHeuristics extends AHeuristicsTemplate
- {
-
- protected function beforeCompute()
- {
- $pairs = iterator_to_array($this->input['pairsGen']());
-
- //sort by price DESC
- usort($pairs, function ($a, $b) {
- return (int)$a['price'] <= (int)$b['price'] ? 1 : -1;
- });
-
- return $pairs;
- }
-
- }
- class PriceVsWeightHeuristics extends AHeuristicsTemplate
- {
- protected function beforeCompute()
- {
- return PriceWeightRatioHelper::run($this->input['pairsGen']);
- }
- }
- class BrutalForceHeuristics extends AHeuristicsTemplate
- {
- protected function beforeCompute()
- {
- return iterator_to_array($this->input['pairsGen']());
- }
-
- protected function compute($pairs)
- {
- $sumPrice = 0;
- $sumWeight = 0;
- $finalItems = [];
-
- //precompute permutations
- //http://stackoverflow.com/a/27160465
-
- $permutations = function (array $elements) use (&$permutations) {
- if (count($elements) <= 1) {
- yield $elements;
- } else {
- foreach ($permutations(array_slice($elements, 1)) as $permutation) {
- foreach (range(0, count($elements) - 1) as $i) {
- yield array_merge(
- array_slice($permutation, 0, $i),
- [$elements[0]],
- array_slice($permutation, $i)
- );
- }
- }
- }
- };
-
- $keys = [];
- foreach ($pairs as $key => $values) {
- $keys[] = $key;
- }
-
- foreach ($permutations($keys) as $keySet) {
- $permPrice = $permWeight = 0;
- $items = [];
- foreach ($keySet as $key) {
- if ($permWeight + $pairs[$key]['weight'] <= $this->input['M']) {
- $permWeight += $pairs[$key]['weight'];
- $permPrice += $pairs[$key]['price'];
- $items[] = $pairs[$key];
- }
- }
-
- if ($permWeight <= $this->input['M'] && $permPrice > $sumPrice) {
- $sumPrice = $permPrice;
- $sumWeight = $permWeight;
- $finalItems = $items;
- }
- }
-
- return [
- 'sumPrice' => $sumPrice,
- 'sumWeight' => $sumWeight,
- 'items' => $finalItems,
- ];
- }
- }
- class NotSoDumbBrutalForce extends AHeuristicsTemplate
- {
- protected function beforeCompute()
- {
- return iterator_to_array($this->input['pairsGen']());
- }
-
- protected function compute($pairs)
- {
- $sumPrice = 0;
- $sumWeight = 0;
- $finalItems = [];
- $max = pow(2, $this->input['n']);
-
- for ($i = 0; $i < $max; $i++) {
- $setupWithoutLeadingZeros = (string)decbin($i);
- $setup = str_repeat("0", $this->input['n'] - strlen($setupWithoutLeadingZeros)) . $setupWithoutLeadingZeros;
- $setupPrice = $setupWeight = 0;
- $items = [];
-
- for ($j = 0; $j < $this->input['n']; $j++) {
- if ($setup[$j] == 1) {
- if ($setupWeight + $pairs[$j]['weight'] <= $this->input['M']) {
- $setupWeight += $pairs[$j]['weight'];
- $setupPrice += $pairs[$j]['price'];
- $items[] = $pairs[$j];
- }
- }
- }
-
- if ($setupWeight <= $this->input['M'] && $setupPrice > $sumPrice) {
- $sumPrice = $setupPrice;
- $sumWeight = $setupWeight;
- $finalItems = $items;
- }
- }
- return [
- 'sumPrice' => $sumPrice,
- 'sumWeight' => $sumWeight,
- 'items' => $finalItems,
- ];
-
- }
-
- }
- class BranchAndBound extends AHeuristicsTemplate
- {
- private $maxBagPrice = 0;
- private $soFarBestPrice = 0;
- private $actualPrice = 0;
-
- private $bagItems;
- private $bestItems;
-
- protected function beforeCompute()
- {
- $this->bagItems = new \SplFixedArray($this->input['n']);
- for ($i = 0; $i < $this->input['n']; $i++) {
- $this->bagItems[$i] = false;
- }
-
- return iterator_to_array($this->input['pairsGen']());
- }
-
- protected function compute($pairs)
- {
- $this->bnbRMP(0, $pairs);
- /*
- foreach ($pairs as $pair) {
- $this->maxBagPrice += $pair['price'];
- }
- $this->bnb(0, $pairs);
- */
-
- return [
- 'sumPrice' => $this->soFarBestPrice,
- 'sumWeight' => -1,
- 'items' => $this->bagItems
- ];
- }
-
- protected function bnb($level, $pairs)
- {
- if ($level == $this->input['n']) {
- $this->getBest($pairs);
- return;
- }
-
- //without
- if ($level + 1 < $this->input['n'] &&
- $this->actualPrice + $this->maxBagPrice - $pairs[$level + 1]['price'] > $this->soFarBestPrice
- ) {
- $this->bnb($level + 1, $pairs);
- }
-
- //with
- if ($this->actualPrice + $this->maxBagPrice > $this->soFarBestPrice) {
- $this->bagItems[$level] = true;
- $this->actualPrice += $pairs[$level]['price'];
- $this->bnb($level + 1, $pairs);
- }
-
- $this->actualPrice -= $pairs[$level]['price'];
- $this->bagItems[$level] = false;
-
- }
-
- protected function bnbRMP($level, $pairs)
- {
- if ($level == $this->input['n']) {
- $this->getBest($pairs);
- return;
- }
-
- //without current item inserted
- if ($this->actualPrice + $this->sumRemainingPrice($level + 1, $pairs) > $this->soFarBestPrice) {
- //bagItems[$level] remains same
- //$this->actualPrice remains same
- $this->bnbRMP($level + 1, $pairs);
- }
-
- //with current item inserted
- if ($this->actualPrice + $this->sumRemainingPrice($level, $pairs) > $this->soFarBestPrice) {
- $this->bagItems[$level] = true;
- $this->actualPrice += $pairs[$level]['price'];
- $this->bnbRMP($level + 1, $pairs);
- }
-
- //before traveling back in up direction in decision tree
- //reset it back to false
- $this->bagItems[$level] = false;
- }
-
- protected function sumRemainingPrice($level, $pairs)
- {
- $rp = 0;
- for ($i = $level; $i < $this->input['n']; $i++) {
- $rp += $pairs[$i]['price'];
- }
- return $rp;
- }
-
- protected function getBest($pairs)
- {
- $price = $weight = 0;
- for ($i = 0; $i < $this->input['n']; $i++) {
- if ($this->bagItems[$i] === true) {
- $weight += $pairs[$i]['weight'];
- $price += $pairs[$i]['price'];
- }
- }
-
- if ($weight <= $this->input['M'] && $price > $this->soFarBestPrice) {
- $this->bestItems = clone $this->bagItems;
- $this->soFarBestPrice = $price;
- }
- }
- }
- class DynamicProgrammingByPrice extends AHeuristicsTemplate
- {
- protected function beforeCompute()
- {
- return iterator_to_array($this->input['pairsGen']());
- }
-
- protected function compute($pairs)
- {
- $maxError = 0;
- $originalPairs = $pairs;
- if($this->shouldRunFPTAS()) {
- $maxPrice = 0;
- foreach ($pairs as $pair) {
- if($pair['price'] > $maxPrice) {
- $maxPrice = $pair['price'];
- }
- }
-
- $b = (int)floor(log($this->input['error'] * $maxPrice / $this->input['n'], 2));
- $maxError = $this->input['n'] * pow(2, $b) / $maxPrice;
-
- $K = $this->input['error'] * $maxPrice / $this->input['n'];
-
- for($i = 0; $i < $this->input['n']; $i++) {
- $pairs[$i]['price'] = (int)floor($pairs[$i]['price'] / $K);
- }
-
- /*echo 'maxPrice: ' . $maxPrice . PHP_EOL;
- echo 'bits: ' . $b . PHP_EOL;
- echo 'me:' . $maxError . PHP_EOL;*/
- }
-
- $totalPrice = 0;
- foreach ($pairs as $pair) {
- $totalPrice += $pair['price'];
- }
- $rows = $totalPrice + 1;
- $cols = $this->input['n'] + 1;
-
- $weights = new \SplFixedArray($rows);
- for ($i = 0; $i < $rows; $i++) {
- $weights[$i] = new \SplFixedArray($cols);
- for ($j = 0; $j < $cols; $j++) {
- $weights[$i][$j] = 0;
- }
- }
-
- for ($col = 0; $col < $cols; $col++) {
- for ($p = 1; $p < $rows; $p++) { //skip 0 row
-
- //set first column to INF
- if ($col === 0) {
- $weights[$p][$col] = INF;
- continue;
- }
-
- $item = $pairs[$col - 1];
-
- //fill first row
- if ($col === 1) {
- $weights[$p][$col] = $p === $item['price'] ? $item['weight'] : INF;
- }
-
- $prevWeightForPrice = $weights[$p][$col - 1];
- $priceIndex = $p - $item['price'];
-
- if ($priceIndex >= 0) {
- if ($weights[$priceIndex][$col - 1] === INF) {
- $weights[$p][$col] = min($prevWeightForPrice, $weights[$priceIndex][$col - 1]);
- } else {
- $weights[$p][$col] = min($prevWeightForPrice, $weights[$priceIndex][$col - 1] + $item['weight']);
- }
- } else {
- $weights[$p][$col] = $prevWeightForPrice;
- }
- }
- }
-
- $bestPrice = 0;
- $sumWeight = 0;
- $lastCol = $cols - 1;
- $currentRow = $totalPrice;
- $column = $this->input['n'];
- $items = [];
-
- //descend from the top rows
- while ($currentRow >= 0) {
- if ($weights[$currentRow][$lastCol] <= $this->input['M']) {
- $bestPrice = $currentRow;
- while ($currentRow > 0) {
- //Jestliže pole (row, col -1) obsahuje váhu stejnou jako pole (row, col), pak row-tá věc není součástí optimálního řešení
- if ($weights[$currentRow][$column] == $weights[$currentRow][$column - 1]) {
- $column--;
- } else {
- $currentRow -= $pairs[$column - 1]['price'];
- $column--;
- $items[$column] = $pairs[$column];
- $sumWeight += $pairs[$column]['weight'];
- }
- }
- break;
- }
- $currentRow--;
- }
-
- //compute final price from original prices
- if($this->shouldRunFPTAS()) {
- $bestPrice = 0;
- foreach($items as $index => $value) {
- $bestPrice += $originalPairs[$index]['price'];
- }
- }
-
- return [
- 'sumPrice' => $bestPrice,
- 'sumWeight' => $sumWeight,
- 'items' => $items,
- 'maxError' => $maxError
- ];
- }
-
- protected function shouldRunFPTAS()
- {
- return $this->input['fptas'] !== false && $this->input['error'] !== false && $this->input['error'] !== 0;
- }
- }
- /**
- * Class BranchAndBoundFractional
- * @package MrCeperka\MIPAA\Heuristics
- * @link http://www.geeksforgeeks.org/branch-and-bound-set-2-implementation-of-01-knapsack/
- * Ported to PHP
- */
- class BranchAndBoundFractional extends AHeuristicsTemplate
- {
- protected function beforeCompute()
- {
- return PriceWeightRatioHelper::run($this->input['pairsGen']);
- }
-
- protected function compute($pairs)
- {
- $root = new Node(true);
- $queue = new \SplQueue();
- $queue->enqueue($root);
- $bestPrice = 0;
-
- while (!$queue->isEmpty()) {
- $node = new Node();
-
- /** @var Node $front */
- $front = $queue->dequeue(); //get the first node
-
- if ($front->level == $this->input['n'] - 1) {
- continue;
- }
-
- //set level 0 if it is a starting node
- $node->level = $front->isRoot ? 0 : $front->level + 1;
-
- //with item
- $node->weight = $front->weight + $pairs[$node->level]['weight'];
- $node->price = $front->price + $pairs[$node->level]['price'];
-
- if ($node->weight <= $this->input['M'] && $bestPrice <= $node->price) {
- $bestPrice = $node->price;
- }
-
- $node->bound = $this->bound($node, $pairs);
-
- if ($node->bound > $bestPrice) {
- $queue->enqueue($node);
- }
-
- //without item
- $node2 = clone $node;
- $node2->weight = $front->weight;
- $node2->price = $front->price;
- $node2->bound = $this->bound($node2, $pairs);
- if ($node2->bound > $bestPrice) {
- $queue->enqueue($node2);
- }
- }
-
- return [
- 'sumPrice' => $bestPrice,
- 'sumWeight' => '',
- 'items' => []
- ];
- }
-
- /**
- * Returns bound of profit in subtree rooted with $node
- * Uses greedy solution + fractions
- * @param Node $node
- * @param $pairs
- * @return float|int
- */
- protected function bound(Node $node, $pairs)
- {
- if ($node->weight >= $this->input['M']) {
- return 0;
- }
-
- $bound = $node->price;
- $index = $node->level + 1;
- $totalWeight = $node->weight;
-
- //weight condition
- while (($index < $this->input['n']) && ($totalWeight + $pairs[$index]['weight'] <= $this->input['M'])) {
- $totalWeight += $pairs[$index]['weight'];
- $bound += $pairs[$index]['price'];
- $index++;
- }
-
- //try to get the fraction of what is left
- if ($index < $this->input['n']) {
- $remainingWeight = $this->input['M'] - $totalWeight;
- //$remainingItemRatio = $pairs[$index]['price'] / max(1, $pairs[$index]['weight']);
- $remainingItemRatio = $pairs[$index]['ratio'];
- $bound += $remainingWeight * $remainingItemRatio;
- }
-
- return $bound;
- }
- }