Bardzo ciekawe zadanie ostatnio dostałem do zrobienia w PHP. Jestem ciekawy jak podeszlibyście do zbudowania takiego algorytmu.

A więc tak:

Zadanie

Dane należy rozbić na 4 kolumny tabeli:
- zachowując porządek alfabetyczny grup,
- nie rozrywając grup na 2 lub więcej kolumn
- jedna grupa musi w całości mieścić się w jednej kolumnie
- wstawiając między grupami jedną linię pustą (po ostatniej kategorii w kolumnie nie trzeba wstawiać pustej linii)

Problem
Kategorie w grupach trzeba rozbić na 4 kolumny, tak aby te kolumny były jak najbardziej zrównoważone pod względem ilości znajdujących się w nich kategorii (i odstępów między nimi), czyli dążymy do tego aby kolumny były jak najkrótsze. Rozwiązanie w postaci skryptu php zawierającego formularz z polem textarea, do którego będzie można wkleić dane wejściowe i po wysłaniu formularza otrzyma się wynik zadania.



Przykładowe dane wejściowe:
Cytat
a

b
b

c
c

d
d
d

e
e


Ma wyświetlić tabelę jak tutaj http://dado.smarthost.pl/test/index.php (przyklad)


Moje (nie idealne) rozwiązanie:
  1. <?php
  2.  
  3. /**
  4. * Klasa sortująca kategorie produktów do tabeli n - kolumnowej
  5. */
  6. class Category
  7. {
  8. /**
  9. * @var array
  10. *
  11. * Parametry do generowania tabeli
  12. */
  13. protected $param = array();
  14.  
  15. /**
  16. * Konstruktor
  17. */
  18. public function __construct()
  19. {
  20. $this->param = array(
  21. 'table_columns' => 4
  22. );
  23. }
  24.  
  25. /**
  26. * Getter
  27. *
  28. * @return val/array/bool
  29. */
  30. public function get_param($val)
  31. {
  32. // dostęp do parametrów klasy
  33. if( ! isset($val)) return false;
  34. if( isset($this->param[$val])) return $this->param[$val];
  35. return false;
  36. }
  37.  
  38. /**
  39. * Spłaszczenie tablicy wejściowej tablicy asocjacyjnej to tablicy jednowymiarowej
  40. *
  41. * @param array $array
  42. * @param integer $cool
  43. *
  44. * @return array
  45. */
  46. protected function merge_group($cool)
  47. {
  48. if(is_array($this->param['render_data']) && count($this->param['render_data']) > 0 && is_numeric($cool))
  49. {
  50. $group_category_array = array();
  51. foreach($this->param['render_data'] as $row)
  52. {
  53. if($row['cool'] == $cool)
  54. {
  55. $group_category_array = array_merge($group_category_array, array_merge($row['category'], array(' ')));
  56. }
  57. }
  58. $this->param['column_max'][] = count($group_category_array);
  59.  
  60. return $group_category_array;
  61. }
  62. else
  63. {
  64. return FALSE;
  65. }
  66. }
  67.  
  68. /**
  69. * Praca na danych, wstępna obróbka
  70. *
  71. * @param string $raw_data
  72. */
  73. private function _data_prepare()
  74. {
  75. if( ! isset($this->param['raw_data']) || empty($this->param['raw_data']))
  76. {
  77. return false;
  78. }
  79.  
  80. $this->param['data_group_arr'] = explode("\n\r", trim($this->param['raw_data'])); // kategorie pogrupowane według nazwy => arr
  81. $this->param['data_arr'] = explode("\r\n", trim($this->param['raw_data'])); // wszystkie kategorie w tablicy 2D
  82. $this->param['count_rows'] = round(count($this->param['data_arr']) / $this->param['table_columns']); // wskaźnik końca danych w jednej kolumnie
  83. }
  84.  
  85. /**
  86. * Podział grup produktów na kolumny
  87. *
  88. * @reutrn array
  89. */
  90. protected function _render_data()
  91. {
  92. $this->_data_prepare(TRUE); // przytowanie danych
  93.  
  94. if( ! isset($this->param['data_group_arr']) || ! is_array($this->param['data_group_arr'])) return false;
  95.  
  96. $i = (int) 0; // elementy w kolumnie
  97. $cool = (int) 1; // iterator kolumn
  98.  
  99. for($tc = 0; $tc <= count( $this->param['data_group_arr'] ); $tc++)
  100. {
  101.  
  102. if( isset( $this->param['data_group_arr'][$tc]))
  103. {
  104. $row = $this->param['data_group_arr'][$tc]; // element do zmiennej lokalnej
  105. $category = explode("\r\n", trim($row)); // kategorie w grupie => array
  106. $count_category = count($category); // obliczenie elementów w kolumnie
  107.  
  108. // bolicznie elementów w kolejnej grupie
  109. if(isset($this->param['data_group_arr'][$tc+1]))
  110. {
  111. $count_category_next = count(explode("\r\n", trim($this->param['data_group_arr'][$tc+1])));
  112. }
  113. else
  114. {
  115. $count_category_next = 0;
  116. }
  117.  
  118. // sumowanie całej kolumny kategorii
  119. $i += $count_category;
  120.  
  121. // generowanie danych do tabeli
  122. $this->param['render_data'][] = array(
  123. 'category' => $category,
  124. 'count' => $count_category,
  125. 'cool' => $cool
  126. );
  127.  
  128. // sprawdzanie czy suma kategorii nie przekracza 1/4 sumy wszytkich pól + sume kategorii grupy następnej
  129. if(($i + $count_category_next) >= $this->param['count_rows'])
  130. {
  131. $i = (int) 0; // zerowanie całości po przekroczeiu wskaźnika
  132. $cool++; // nowa kolumna
  133. }
  134. }
  135. }
  136. return TRUE;
  137. }
  138. }
  139.  
  140. /**
  141. * Zwracanie sforamtowanych danych dla widoku
  142. */
  143. class Category_Data extends Category
  144. {
  145.  
  146. /**
  147. * Pobranie danych po obróbce, zwraca tablice w 4 głównych elementach
  148. *
  149. * @return array
  150. */
  151. public function get_final_data($raw_data)
  152. {
  153.  
  154. $column = array();
  155. $this->param['raw_data'] = trim(strip_tags(htmlspecialchars($raw_data))) ; // filtrowanie superglobalnej
  156.  
  157. if($this->_render_data() === TRUE)
  158. {
  159. for($i=1; $i<=($this->param['table_columns'] ); $i++)
  160. {
  161. $column['col_' . $i] = $this->merge_group($i);
  162. }
  163. return $column;
  164. }
  165. else
  166. {
  167. return false;
  168. }
  169. }
  170. }
  171.  
  172.  
  173.  
  174. /**
  175. * Mini kontroler na potrzeby zadania
  176. */
  177. class Controller
  178. {
  179.  
  180. /**
  181. * @var string
  182. *
  183. * akcja kontrolera
  184. */
  185. private $action;
  186.  
  187. /**
  188. * @var array
  189. *
  190. * Przechowywanie widoku
  191. */
  192. private $template = array();
  193.  
  194. /**
  195. * Inicjuje zmienne w tablicy widoku, filtruje zmienną globalną
  196. * twoży nazwę publicznej metody kontrolera i jeśli istnieje wywołuje ją
  197. */
  198. public function __construct()
  199. {
  200. // elementy widoku
  201. $this->template['head'] = '<!DOCTYPE HTML>
  202. <html>
  203. <meta charset="utf-8">
  204. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  205. <meta name="viewport" content="width=device-width, initial-scale=1">
  206. <title>Kategorie Produktów</title>
  207. <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
  208. <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css">
  209. </head>
  210. <body style="padding:2em;"><div class="container"><div class="row">
  211. <h1>Zadanie: Kategorie Produktów</h1>
  212. </div>';
  213. $this->template['footer'] = '</div></body></html>';
  214. $this->template['back'] = '<br /><a href="index.php" class="btn btn-primary" role="button">Powrót</a>';
  215. // elementy widoku - koniec
  216.  
  217. if( ! isset($_GET['action']) || empty($_GET['action'])) $_GET['action'] = 'index'; // predefiniowanie
  218. $this->action = trim(strip_tags( htmlspecialchars($_GET['action']))) . '_action'; // Filtrowanie
  219.  
  220. if( is_callable(array($this, $this->action)))
  221. {
  222. return call_user_func(array($this, $this->action)); // Call Action
  223. }
  224. else
  225. {
  226. return $this->notfound_action(); // HTTP 404
  227. }
  228. }
  229.  
  230.  
  231. /**
  232. * Wynik zadania
  233. *
  234. * @return string
  235. */
  236. public function render_action()
  237. {
  238. // Pobieranie wyniku zadania
  239. $category = new Category_Data;
  240. $data = $category->get_final_data($_POST['raw_data']); // pobieranie danych
  241.  
  242. // Widok
  243. echo $this->template['head'];
  244. if($data !== FALSE)
  245. {
  246. $table_columns = $category->get_param('table_columns'); // pobieranie liczby kolumn
  247. $count_rows = max($category->get_param('column_max')) - 2; // pobieranie maksymalnej liczby wierszy w kolumnie;
  248.  
  249. $this->template['table'] = '<table class="table table-bordered">' ;
  250. for( $i=0; $i<=$count_rows; $i++ )
  251. {
  252. $this->template['table'] .= '<tr>';
  253. for($col=1; $col<=$table_columns; $col++)
  254. {
  255. $this->template['table'] .= (isset( $data['col_' . $col][$i]) ? '<td>'.$data['col_' . $col][$i].'</td>':'<td></td>');
  256. }
  257. $this->template['table'] .= '</tr>';
  258. }
  259. $this->template['table'] .= '</table>';
  260. echo $this->template['table'];
  261. }
  262. else
  263. {
  264. echo '<div class="alert alert-danger" role="alert"><b>Błąd!</b> Brak danych.</div>';
  265. }
  266. echo $this->template['back'] . $this->template['footer'];
  267. }
  268.  
  269. /**
  270. * Akcja domyślna
  271. *
  272. * @return string
  273. */
  274. public function index_action()
  275. {
  276. // Widok
  277. echo $this->template['head'] .
  278. '<div class="row">
  279. <div class="col-md-12">
  280. <form method="post" action="index.php?action=render" class="form-horizontal">
  281. <div class="form-group">
  282. <label>Dane wejściowe</label>
  283. <textarea name="raw_data" class="form-control" rows="23"></textarea><br />
  284. <button type="submit" class="btn btn-primary">Wyślij</button>
  285. </div>
  286. </form>
  287. </div>
  288. </div>' . $this->template['footer'];
  289. }
  290.  
  291.  
  292. /**
  293. * HTTP 404
  294. *
  295. * @return string
  296. */
  297. public function notfound_action()
  298. {
  299. header('HTTP/1.0 404 Not Found');
  300. echo $this->template['head'] .
  301. '<div class="alert alert-danger" role="alert"><h1>404 Not Found</h1></div>' .
  302. $this->template['back'] .
  303. $this->template['footer'];
  304. }
  305.  
  306. }
  307.  
  308. $Controller = new Controller; // Uruchomienie zadania
  309. ?>
  310.