PageRenderTime 51ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/Heuristics.php

https://gitlab.com/mrceperka/mi-paa
PHP | 598 lines | 453 code | 98 blank | 47 comment | 66 complexity | 75d5705fab3d1ecfd98d9bbd2b6efd1e MD5 | raw file
  1. <?php
  2. namespace MrCeperka\MIPAA\Heuristics;
  3. require_once 'Helpers.php';
  4. use MrCeperka\MIPAA\Helpers\Node;
  5. use MrCeperka\MIPAA\Helpers\PriceWeightRatioHelper;
  6. interface IHeuristics
  7. {
  8. public function execute();
  9. public function getResult();
  10. public function logResult();
  11. }
  12. abstract class AHeuristicsTemplate implements IHeuristics
  13. {
  14. protected $result = [];
  15. protected $input;
  16. public function __construct($input)
  17. {
  18. $this->input = $input;
  19. }
  20. public final function execute()
  21. {
  22. $start = microtime(true);
  23. $pairs = $this->beforeCompute();
  24. $r = $this->compute($pairs);
  25. $end = microtime(true) - $start;
  26. $end *= 1000 * 1000;
  27. $this->result = array_merge(
  28. [
  29. 'ID' => $this->input['ID'],
  30. 'n' => $this->input['n'],
  31. 'M' => $this->input['M'],
  32. 'time' => round($end),
  33. ],
  34. $r
  35. );
  36. }
  37. public function getResult()
  38. {
  39. return $this->result;
  40. }
  41. protected abstract function beforeCompute();
  42. protected function compute($pairs)
  43. {
  44. $sumPrice = 0;
  45. $sumWeight = 0;
  46. $items = [];
  47. for ($i = 0; $i < $this->input['n']; $i++) {
  48. if ($sumWeight + $pairs[$i]['weight'] <= $this->input['M']) {
  49. $sumPrice += $pairs[$i]['price'];
  50. $sumWeight += $pairs[$i]['weight'];
  51. $items[] = $pairs[$i];
  52. } else {
  53. //try to find another item with the biggest price
  54. for ($j = $i; $j < $this->input['n']; $j++) {
  55. if ($sumWeight + $pairs[$i]['weight'] <= $this->input['M']) {
  56. $sumPrice += $pairs[$i]['price'];
  57. $sumWeight += $pairs[$i]['weight'];
  58. $items[] = $pairs[$i];
  59. }
  60. }
  61. break;
  62. }
  63. }
  64. return [
  65. 'sumPrice' => $sumPrice,
  66. 'sumWeight' => $sumWeight,
  67. 'items' => $items,
  68. ];
  69. }
  70. public function logResult()
  71. {
  72. $logString = '-------------------------------' . "\n";
  73. $logString .= implode(' ', [$this->result['ID'], $this->result['n'], $this->result['M']]) . "\n";
  74. $logString .= $this->result['ID'] . ' sumPrice: ' . $this->result['sumPrice'] . ' sumWeight: ' . $this->result['sumWeight'] . "\n";
  75. //generate map of items in bag
  76. $resultHashed = [];
  77. $outcome = [];
  78. foreach ($this->result['items'] as $pair) {
  79. $resultHashed[$this->formatKey($pair)] = $pair;
  80. }
  81. foreach ($this->input['pairsGen']() as $pair) {
  82. if (!isset($resultHashed[$this->formatKey($pair)])) {
  83. $outcome[$this->formatKey($pair)] = '0';
  84. } else {
  85. $outcome[$this->formatKey($pair)] = '1';
  86. }
  87. }
  88. $logString .= 'itemsMap: ' . "\n";
  89. foreach ($outcome as $key => $value) {
  90. $logString .= $key . ' => ' . $value . "\n";
  91. }
  92. $logString .= '-------------------------------' . "\n";
  93. file_put_contents(OUTPUT_DIR . strtolower((new \ReflectionClass($this))->getShortName()) . '.log', $logString, FILE_APPEND);
  94. }
  95. protected function formatKey($pair)
  96. {
  97. return $pair['price'] . ' : ' . $pair['weight'];
  98. }
  99. }
  100. class MaxPriceHeuristics extends AHeuristicsTemplate
  101. {
  102. protected function beforeCompute()
  103. {
  104. $pairs = iterator_to_array($this->input['pairsGen']());
  105. //sort by price DESC
  106. usort($pairs, function ($a, $b) {
  107. return (int)$a['price'] <= (int)$b['price'] ? 1 : -1;
  108. });
  109. return $pairs;
  110. }
  111. }
  112. class PriceVsWeightHeuristics extends AHeuristicsTemplate
  113. {
  114. protected function beforeCompute()
  115. {
  116. return PriceWeightRatioHelper::run($this->input['pairsGen']);
  117. }
  118. }
  119. class BrutalForceHeuristics extends AHeuristicsTemplate
  120. {
  121. protected function beforeCompute()
  122. {
  123. return iterator_to_array($this->input['pairsGen']());
  124. }
  125. protected function compute($pairs)
  126. {
  127. $sumPrice = 0;
  128. $sumWeight = 0;
  129. $finalItems = [];
  130. //precompute permutations
  131. //http://stackoverflow.com/a/27160465
  132. $permutations = function (array $elements) use (&$permutations) {
  133. if (count($elements) <= 1) {
  134. yield $elements;
  135. } else {
  136. foreach ($permutations(array_slice($elements, 1)) as $permutation) {
  137. foreach (range(0, count($elements) - 1) as $i) {
  138. yield array_merge(
  139. array_slice($permutation, 0, $i),
  140. [$elements[0]],
  141. array_slice($permutation, $i)
  142. );
  143. }
  144. }
  145. }
  146. };
  147. $keys = [];
  148. foreach ($pairs as $key => $values) {
  149. $keys[] = $key;
  150. }
  151. foreach ($permutations($keys) as $keySet) {
  152. $permPrice = $permWeight = 0;
  153. $items = [];
  154. foreach ($keySet as $key) {
  155. if ($permWeight + $pairs[$key]['weight'] <= $this->input['M']) {
  156. $permWeight += $pairs[$key]['weight'];
  157. $permPrice += $pairs[$key]['price'];
  158. $items[] = $pairs[$key];
  159. }
  160. }
  161. if ($permWeight <= $this->input['M'] && $permPrice > $sumPrice) {
  162. $sumPrice = $permPrice;
  163. $sumWeight = $permWeight;
  164. $finalItems = $items;
  165. }
  166. }
  167. return [
  168. 'sumPrice' => $sumPrice,
  169. 'sumWeight' => $sumWeight,
  170. 'items' => $finalItems,
  171. ];
  172. }
  173. }
  174. class NotSoDumbBrutalForce extends AHeuristicsTemplate
  175. {
  176. protected function beforeCompute()
  177. {
  178. return iterator_to_array($this->input['pairsGen']());
  179. }
  180. protected function compute($pairs)
  181. {
  182. $sumPrice = 0;
  183. $sumWeight = 0;
  184. $finalItems = [];
  185. $max = pow(2, $this->input['n']);
  186. for ($i = 0; $i < $max; $i++) {
  187. $setupWithoutLeadingZeros = (string)decbin($i);
  188. $setup = str_repeat("0", $this->input['n'] - strlen($setupWithoutLeadingZeros)) . $setupWithoutLeadingZeros;
  189. $setupPrice = $setupWeight = 0;
  190. $items = [];
  191. for ($j = 0; $j < $this->input['n']; $j++) {
  192. if ($setup[$j] == 1) {
  193. if ($setupWeight + $pairs[$j]['weight'] <= $this->input['M']) {
  194. $setupWeight += $pairs[$j]['weight'];
  195. $setupPrice += $pairs[$j]['price'];
  196. $items[] = $pairs[$j];
  197. }
  198. }
  199. }
  200. if ($setupWeight <= $this->input['M'] && $setupPrice > $sumPrice) {
  201. $sumPrice = $setupPrice;
  202. $sumWeight = $setupWeight;
  203. $finalItems = $items;
  204. }
  205. }
  206. return [
  207. 'sumPrice' => $sumPrice,
  208. 'sumWeight' => $sumWeight,
  209. 'items' => $finalItems,
  210. ];
  211. }
  212. }
  213. class BranchAndBound extends AHeuristicsTemplate
  214. {
  215. private $maxBagPrice = 0;
  216. private $soFarBestPrice = 0;
  217. private $actualPrice = 0;
  218. private $bagItems;
  219. private $bestItems;
  220. protected function beforeCompute()
  221. {
  222. $this->bagItems = new \SplFixedArray($this->input['n']);
  223. for ($i = 0; $i < $this->input['n']; $i++) {
  224. $this->bagItems[$i] = false;
  225. }
  226. return iterator_to_array($this->input['pairsGen']());
  227. }
  228. protected function compute($pairs)
  229. {
  230. $this->bnbRMP(0, $pairs);
  231. /*
  232. foreach ($pairs as $pair) {
  233. $this->maxBagPrice += $pair['price'];
  234. }
  235. $this->bnb(0, $pairs);
  236. */
  237. return [
  238. 'sumPrice' => $this->soFarBestPrice,
  239. 'sumWeight' => -1,
  240. 'items' => $this->bagItems
  241. ];
  242. }
  243. protected function bnb($level, $pairs)
  244. {
  245. if ($level == $this->input['n']) {
  246. $this->getBest($pairs);
  247. return;
  248. }
  249. //without
  250. if ($level + 1 < $this->input['n'] &&
  251. $this->actualPrice + $this->maxBagPrice - $pairs[$level + 1]['price'] > $this->soFarBestPrice
  252. ) {
  253. $this->bnb($level + 1, $pairs);
  254. }
  255. //with
  256. if ($this->actualPrice + $this->maxBagPrice > $this->soFarBestPrice) {
  257. $this->bagItems[$level] = true;
  258. $this->actualPrice += $pairs[$level]['price'];
  259. $this->bnb($level + 1, $pairs);
  260. }
  261. $this->actualPrice -= $pairs[$level]['price'];
  262. $this->bagItems[$level] = false;
  263. }
  264. protected function bnbRMP($level, $pairs)
  265. {
  266. if ($level == $this->input['n']) {
  267. $this->getBest($pairs);
  268. return;
  269. }
  270. //without current item inserted
  271. if ($this->actualPrice + $this->sumRemainingPrice($level + 1, $pairs) > $this->soFarBestPrice) {
  272. //bagItems[$level] remains same
  273. //$this->actualPrice remains same
  274. $this->bnbRMP($level + 1, $pairs);
  275. }
  276. //with current item inserted
  277. if ($this->actualPrice + $this->sumRemainingPrice($level, $pairs) > $this->soFarBestPrice) {
  278. $this->bagItems[$level] = true;
  279. $this->actualPrice += $pairs[$level]['price'];
  280. $this->bnbRMP($level + 1, $pairs);
  281. }
  282. //before traveling back in up direction in decision tree
  283. //reset it back to false
  284. $this->bagItems[$level] = false;
  285. }
  286. protected function sumRemainingPrice($level, $pairs)
  287. {
  288. $rp = 0;
  289. for ($i = $level; $i < $this->input['n']; $i++) {
  290. $rp += $pairs[$i]['price'];
  291. }
  292. return $rp;
  293. }
  294. protected function getBest($pairs)
  295. {
  296. $price = $weight = 0;
  297. for ($i = 0; $i < $this->input['n']; $i++) {
  298. if ($this->bagItems[$i] === true) {
  299. $weight += $pairs[$i]['weight'];
  300. $price += $pairs[$i]['price'];
  301. }
  302. }
  303. if ($weight <= $this->input['M'] && $price > $this->soFarBestPrice) {
  304. $this->bestItems = clone $this->bagItems;
  305. $this->soFarBestPrice = $price;
  306. }
  307. }
  308. }
  309. class DynamicProgrammingByPrice extends AHeuristicsTemplate
  310. {
  311. protected function beforeCompute()
  312. {
  313. return iterator_to_array($this->input['pairsGen']());
  314. }
  315. protected function compute($pairs)
  316. {
  317. $maxError = 0;
  318. $originalPairs = $pairs;
  319. if($this->shouldRunFPTAS()) {
  320. $maxPrice = 0;
  321. foreach ($pairs as $pair) {
  322. if($pair['price'] > $maxPrice) {
  323. $maxPrice = $pair['price'];
  324. }
  325. }
  326. $b = (int)floor(log($this->input['error'] * $maxPrice / $this->input['n'], 2));
  327. $maxError = $this->input['n'] * pow(2, $b) / $maxPrice;
  328. $K = $this->input['error'] * $maxPrice / $this->input['n'];
  329. for($i = 0; $i < $this->input['n']; $i++) {
  330. $pairs[$i]['price'] = (int)floor($pairs[$i]['price'] / $K);
  331. }
  332. /*echo 'maxPrice: ' . $maxPrice . PHP_EOL;
  333. echo 'bits: ' . $b . PHP_EOL;
  334. echo 'me:' . $maxError . PHP_EOL;*/
  335. }
  336. $totalPrice = 0;
  337. foreach ($pairs as $pair) {
  338. $totalPrice += $pair['price'];
  339. }
  340. $rows = $totalPrice + 1;
  341. $cols = $this->input['n'] + 1;
  342. $weights = new \SplFixedArray($rows);
  343. for ($i = 0; $i < $rows; $i++) {
  344. $weights[$i] = new \SplFixedArray($cols);
  345. for ($j = 0; $j < $cols; $j++) {
  346. $weights[$i][$j] = 0;
  347. }
  348. }
  349. for ($col = 0; $col < $cols; $col++) {
  350. for ($p = 1; $p < $rows; $p++) { //skip 0 row
  351. //set first column to INF
  352. if ($col === 0) {
  353. $weights[$p][$col] = INF;
  354. continue;
  355. }
  356. $item = $pairs[$col - 1];
  357. //fill first row
  358. if ($col === 1) {
  359. $weights[$p][$col] = $p === $item['price'] ? $item['weight'] : INF;
  360. }
  361. $prevWeightForPrice = $weights[$p][$col - 1];
  362. $priceIndex = $p - $item['price'];
  363. if ($priceIndex >= 0) {
  364. if ($weights[$priceIndex][$col - 1] === INF) {
  365. $weights[$p][$col] = min($prevWeightForPrice, $weights[$priceIndex][$col - 1]);
  366. } else {
  367. $weights[$p][$col] = min($prevWeightForPrice, $weights[$priceIndex][$col - 1] + $item['weight']);
  368. }
  369. } else {
  370. $weights[$p][$col] = $prevWeightForPrice;
  371. }
  372. }
  373. }
  374. $bestPrice = 0;
  375. $sumWeight = 0;
  376. $lastCol = $cols - 1;
  377. $currentRow = $totalPrice;
  378. $column = $this->input['n'];
  379. $items = [];
  380. //descend from the top rows
  381. while ($currentRow >= 0) {
  382. if ($weights[$currentRow][$lastCol] <= $this->input['M']) {
  383. $bestPrice = $currentRow;
  384. while ($currentRow > 0) {
  385. //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í
  386. if ($weights[$currentRow][$column] == $weights[$currentRow][$column - 1]) {
  387. $column--;
  388. } else {
  389. $currentRow -= $pairs[$column - 1]['price'];
  390. $column--;
  391. $items[$column] = $pairs[$column];
  392. $sumWeight += $pairs[$column]['weight'];
  393. }
  394. }
  395. break;
  396. }
  397. $currentRow--;
  398. }
  399. //compute final price from original prices
  400. if($this->shouldRunFPTAS()) {
  401. $bestPrice = 0;
  402. foreach($items as $index => $value) {
  403. $bestPrice += $originalPairs[$index]['price'];
  404. }
  405. }
  406. return [
  407. 'sumPrice' => $bestPrice,
  408. 'sumWeight' => $sumWeight,
  409. 'items' => $items,
  410. 'maxError' => $maxError
  411. ];
  412. }
  413. protected function shouldRunFPTAS()
  414. {
  415. return $this->input['fptas'] !== false && $this->input['error'] !== false && $this->input['error'] !== 0;
  416. }
  417. }
  418. /**
  419. * Class BranchAndBoundFractional
  420. * @package MrCeperka\MIPAA\Heuristics
  421. * @link http://www.geeksforgeeks.org/branch-and-bound-set-2-implementation-of-01-knapsack/
  422. * Ported to PHP
  423. */
  424. class BranchAndBoundFractional extends AHeuristicsTemplate
  425. {
  426. protected function beforeCompute()
  427. {
  428. return PriceWeightRatioHelper::run($this->input['pairsGen']);
  429. }
  430. protected function compute($pairs)
  431. {
  432. $root = new Node(true);
  433. $queue = new \SplQueue();
  434. $queue->enqueue($root);
  435. $bestPrice = 0;
  436. while (!$queue->isEmpty()) {
  437. $node = new Node();
  438. /** @var Node $front */
  439. $front = $queue->dequeue(); //get the first node
  440. if ($front->level == $this->input['n'] - 1) {
  441. continue;
  442. }
  443. //set level 0 if it is a starting node
  444. $node->level = $front->isRoot ? 0 : $front->level + 1;
  445. //with item
  446. $node->weight = $front->weight + $pairs[$node->level]['weight'];
  447. $node->price = $front->price + $pairs[$node->level]['price'];
  448. if ($node->weight <= $this->input['M'] && $bestPrice <= $node->price) {
  449. $bestPrice = $node->price;
  450. }
  451. $node->bound = $this->bound($node, $pairs);
  452. if ($node->bound > $bestPrice) {
  453. $queue->enqueue($node);
  454. }
  455. //without item
  456. $node2 = clone $node;
  457. $node2->weight = $front->weight;
  458. $node2->price = $front->price;
  459. $node2->bound = $this->bound($node2, $pairs);
  460. if ($node2->bound > $bestPrice) {
  461. $queue->enqueue($node2);
  462. }
  463. }
  464. return [
  465. 'sumPrice' => $bestPrice,
  466. 'sumWeight' => '',
  467. 'items' => []
  468. ];
  469. }
  470. /**
  471. * Returns bound of profit in subtree rooted with $node
  472. * Uses greedy solution + fractions
  473. * @param Node $node
  474. * @param $pairs
  475. * @return float|int
  476. */
  477. protected function bound(Node $node, $pairs)
  478. {
  479. if ($node->weight >= $this->input['M']) {
  480. return 0;
  481. }
  482. $bound = $node->price;
  483. $index = $node->level + 1;
  484. $totalWeight = $node->weight;
  485. //weight condition
  486. while (($index < $this->input['n']) && ($totalWeight + $pairs[$index]['weight'] <= $this->input['M'])) {
  487. $totalWeight += $pairs[$index]['weight'];
  488. $bound += $pairs[$index]['price'];
  489. $index++;
  490. }
  491. //try to get the fraction of what is left
  492. if ($index < $this->input['n']) {
  493. $remainingWeight = $this->input['M'] - $totalWeight;
  494. //$remainingItemRatio = $pairs[$index]['price'] / max(1, $pairs[$index]['weight']);
  495. $remainingItemRatio = $pairs[$index]['ratio'];
  496. $bound += $remainingWeight * $remainingItemRatio;
  497. }
  498. return $bound;
  499. }
  500. }