1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* @author ventaquil <[email protected]> |
5
|
|
|
* @licence MIT |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace PHPAlgorithms; |
9
|
|
|
|
10
|
|
|
use Closure; |
11
|
|
|
use PHPAlgorithms\Dijkstra\Creator; |
12
|
|
|
use PHPAlgorithms\Dijkstra\Exceptions\Exception; |
13
|
|
|
use PHPAlgorithms\Dijkstra\Path; |
14
|
|
|
use PHPAlgorithms\Dijkstra\Point; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* Class Dijkstra |
18
|
|
|
* @package PHPAlgorithms |
19
|
|
|
* @property array $results |
20
|
|
|
*/ |
21
|
|
|
class Dijkstra { |
22
|
|
|
/** |
23
|
|
|
* @var array $points Array of collected Point objects. |
24
|
|
|
* @var array $results Array of generated results. |
25
|
|
|
*/ |
26
|
|
|
private $points = array(); |
27
|
|
|
private $results = array(); |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Dijkstra constructor. |
31
|
|
|
* |
32
|
|
|
* @param Closure|null $function Anonymous function to create relations. |
33
|
|
|
*/ |
34
|
|
|
public function __construct($function = null) |
35
|
|
|
{ |
36
|
|
|
if (!is_null($function)) { |
37
|
|
|
$this->add($function); |
38
|
|
|
} |
39
|
|
|
} |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Method executes sent function and add result to $points parameter. |
43
|
|
|
* |
44
|
|
|
* @param Closure $function Anonymous function to create relations. |
45
|
|
|
* @return Dijkstra $this Reference to the same object. |
46
|
|
|
* @throws Exception Method throws exception when $function argument isn't a Closure object. |
47
|
|
|
*/ |
48
|
|
|
public function add($function) |
49
|
|
|
{ |
50
|
|
|
if (!($function instanceof Closure)) { |
51
|
|
|
throw new Exception('Sent argument is not instance of Closure'); |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
$creator = new Creator(); |
55
|
|
|
$function($creator); |
56
|
|
|
|
57
|
|
|
if (empty($this->points)) { |
58
|
|
|
$this->points = $creator->dumpPoints(); |
59
|
|
|
} else { |
60
|
|
|
$index = count($this->points); |
61
|
|
|
|
62
|
|
|
foreach ($creator->dumpPoints() as $point) { |
63
|
|
|
$this->points[$index] = $point; |
64
|
|
|
$point->id = $index; |
65
|
|
|
|
66
|
|
|
$index++; |
67
|
|
|
} |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
return $this; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Method search unvisited points in neighborhood of sent point. |
75
|
|
|
* |
76
|
|
|
* @param integer[] $visited Array of visited points' ids. |
77
|
|
|
* @param integer $point Point id. |
78
|
|
|
* @return boolean True when exists unvisited point in neighborhood or false otherwise. |
79
|
|
|
*/ |
80
|
|
|
private function existsUnvisitedInNeighborhood($visited, $point) |
81
|
|
|
{ |
82
|
|
|
foreach ($this->points[$point]->relations as $relation) { |
83
|
|
|
if (!isset($visited[$relation->to->id])) { |
84
|
|
|
return true; |
85
|
|
|
} |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
return false; |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* @param integer[] $unvisited Array of unvisited points' ids. |
93
|
|
|
* @param integer[] $visited Array of visited points' ids. |
94
|
|
|
* @param integer|null $startPoint Start point id. |
95
|
|
|
* @return integer|null Method returns point identifier where it exists in $unvisited array or null otherwise. |
96
|
|
|
*/ |
97
|
|
|
private function findPointWithUncheckedRelation($unvisited, $visited, $startPoint = null) |
98
|
|
|
{ |
99
|
|
|
if (empty($unvisited)) { |
100
|
|
|
return null; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
if (is_null($startPoint)) { |
104
|
|
|
foreach ($this->points as $point) { |
105
|
|
|
if ($this->existsUnvisitedInNeighborhood($visited, $point->id)) { |
106
|
|
|
return $point->id; |
107
|
|
|
} |
108
|
|
|
} |
109
|
|
|
} elseif ($this->existsUnvisitedInNeighborhood($visited, $startPoint)) { |
110
|
|
|
return $startPoint; |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
return null; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* Method generates path for given Point object or point id. |
118
|
|
|
* |
119
|
|
|
* @param Point|integer $point Point object or point identification number. |
120
|
|
|
* @return Path[] Array of Path objects. |
121
|
|
|
* @throws Exception Throws exception when sent point not isset in object's $point array. |
122
|
|
|
*/ |
123
|
|
|
public function generate($point) |
124
|
|
|
{ |
125
|
|
|
$point = $this->getPointID($point); |
126
|
|
|
|
127
|
|
|
if (empty($this->results[$point])) { |
128
|
|
|
return $this->generateForce($point); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
return $this->results[$point]; |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* Method generates paths for all defined points. |
136
|
|
|
* |
137
|
|
|
* @return Path[][] Two-dimensional array of Path objects. |
138
|
|
|
* @throws Exception Method throws exception when generate() method throws exception. |
139
|
|
|
*/ |
140
|
|
View Code Duplication |
public function generateAll() |
|
|
|
|
141
|
|
|
{ |
142
|
|
|
$generated = array(); |
143
|
|
|
|
144
|
|
|
foreach ($this->points as $index => $point) { |
145
|
|
|
$generated[$index] = $this->generate($point); |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
return $generated; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* Method generates path for given Point object or point id. It's not watching for existing cache. |
153
|
|
|
* |
154
|
|
|
* @param Point|integer $point Point object or point identification number. |
155
|
|
|
* @return Path[] Array of Path objects. |
156
|
|
|
* @throws Exception Throws exception when sent point not isset in object's $point array. |
157
|
|
|
*/ |
158
|
|
|
public function generateForce($point) |
159
|
|
|
{ |
160
|
|
|
$point = $this->getPointID($point); |
161
|
|
|
|
162
|
|
|
$startPoint = $point; |
163
|
|
|
|
164
|
|
|
$visited = array(); |
165
|
|
|
$keys = array_keys($this->points); |
166
|
|
|
$unvisited = array_combine($keys, $keys); |
167
|
|
|
$paths = array(); |
168
|
|
|
$previous = null; |
169
|
|
|
|
170
|
|
|
while (!empty($unvisited) && !is_null($point)) { |
171
|
|
|
unset($unvisited[$point]); |
172
|
|
|
$visited[$point] = $point; |
173
|
|
|
|
174
|
|
|
if (!isset($paths[$point])) { |
175
|
|
|
if (is_null($previous)) { |
176
|
|
|
$paths[$point] = new Path(); |
177
|
|
|
} else { |
178
|
|
|
$paths[$point]->copy($paths[$previous]); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
$paths[$point]->addNode($point); |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
$relation = $this->getMinimalRelation($unvisited, $paths, $point); |
185
|
|
|
|
186
|
|
|
$this->updatePaths($paths, $point); |
187
|
|
|
|
188
|
|
|
$previous = $point; |
189
|
|
|
|
190
|
|
|
if (!is_null($relation)) { |
191
|
|
|
$point = $relation->to |
|
|
|
|
192
|
|
|
->id; |
193
|
|
|
} else { |
194
|
|
|
$point = $this->findPointWithUncheckedRelation($unvisited, $visited, $startPoint); |
195
|
|
|
} |
196
|
|
|
}; |
197
|
|
|
|
198
|
|
|
return $paths; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* Method generates paths for all defined points. It's not watching for existing cache. |
203
|
|
|
* |
204
|
|
|
* @return Path[][] Two-dimensional array of Path objects. |
205
|
|
|
* @throws Exception Method throws exception when generate() method throws exception. |
206
|
|
|
*/ |
207
|
|
View Code Duplication |
public function generateForceAll() |
|
|
|
|
208
|
|
|
{ |
209
|
|
|
$generated = array(); |
210
|
|
|
|
211
|
|
|
foreach ($this->points as $index => $point) { |
212
|
|
|
$generated[$index] = $this->generateForce($point); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
return $generated; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Method try to find minimal relation to another point from given point. |
220
|
|
|
* |
221
|
|
|
* @param integer[] $unvisited Array of unvisited points' ids. |
222
|
|
|
* @param Path[] $paths Array of generated Path objects. |
223
|
|
|
* @param integer $point Point integer identifier. |
224
|
|
|
* @return Dijkstra\Relation|null Method returns Relation object or null when minimal relation wasn't found. |
225
|
|
|
*/ |
226
|
|
|
private function getMinimalRelation($unvisited, $paths, $point) |
227
|
|
|
{ |
228
|
|
|
$minimalValue = INF; |
229
|
|
|
$minimalIndex = -1; |
230
|
|
|
|
231
|
|
|
foreach ($this->points[$point]->relations as $index => $relation) { |
232
|
|
|
if (($minimalValue > ($relation->distance + $paths[$point]->distance)) && isset($unvisited[$relation->to->id])) { |
233
|
|
|
$minimalIndex = $index; |
234
|
|
|
$minimalValue = $relation->distance + $paths[$point]->distance; |
235
|
|
|
} |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
if ($minimalIndex != -1) { |
239
|
|
|
return $this->points[$point] |
240
|
|
|
->relations[$minimalIndex]; |
241
|
|
|
} else { |
242
|
|
|
return null; |
243
|
|
|
} |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* Method gets from Point object identifier or checks given value. If this value not exists in $points object property then throws exception. |
248
|
|
|
* |
249
|
|
|
* @param mixed $point Value to check. |
250
|
|
|
* @return integer Point identifier. |
251
|
|
|
* @throws Exception Method throws exception when given value not exists in $points array. |
252
|
|
|
*/ |
253
|
|
|
private function getPointID($point) |
254
|
|
|
{ |
255
|
|
|
if ($point instanceof Point) { |
256
|
|
|
$point = $point->id; |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
if (!isset($this->points[$point])) { |
260
|
|
|
throw new Exception("Point with id {$point} not exists"); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
return $point; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* @param array $reIndex New points' indexes. |
268
|
|
|
*/ |
269
|
|
|
public function reIndex($reIndex) |
270
|
|
|
{ |
271
|
|
|
if (array_keys($reIndex) != array_keys($this->points)) { |
272
|
|
|
throw new Exception('You don\'t reindex all keys'); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
foreach ($reIndex as $oldIndex => $newIndex) { |
276
|
|
|
foreach ($reIndex as $oldIndex_ => $newIndex_) { |
277
|
|
|
if (($oldIndex != $oldIndex_) && ($newIndex == $newIndex_)) { |
278
|
|
|
throw new Exception('Array error: two different indexes show to one'); |
279
|
|
|
} |
280
|
|
|
} |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
$points = array(); |
284
|
|
|
$results = array(); |
285
|
|
|
foreach ($reIndex as $oldIndex => $newIndex) { |
286
|
|
|
$points[$newIndex] = $this->points[$oldIndex]; |
287
|
|
|
$points[$newIndex]->id = $newIndex; |
288
|
|
|
|
289
|
|
|
if (isset($this->results[$oldIndex])) { |
290
|
|
|
$results[$newIndex] = $this->results[$oldIndex]; |
291
|
|
|
} |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
$this->points = $points; |
295
|
|
|
$this->results = $results; |
296
|
|
|
|
297
|
|
|
return $this; |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
/** |
301
|
|
|
* Method updates existing Path object or create new if it's possible. |
302
|
|
|
* |
303
|
|
|
* @param Path[] &$paths Array of generated Path[] objects. Given by reference. |
304
|
|
|
* @param integer $point Integer point identifier. |
305
|
|
|
*/ |
306
|
|
|
public function updatePaths(&$paths, $point) |
307
|
|
|
{ |
308
|
|
|
foreach ($this->points[$point]->relations as $relation) { |
309
|
|
|
if (isset($paths[$relation->to->id])) { |
310
|
|
|
if ($paths[$relation->to->id]->distance > ($paths[$point]->distance + $relation->distance)) { |
311
|
|
|
$paths[$relation->to->id]->copy($paths[$point]) |
312
|
|
|
->addNode($relation->to->id, $relation->distance); |
313
|
|
|
} |
314
|
|
|
} else { |
315
|
|
|
$paths[$relation->to->id] = (new Path())->copy($paths[$point]) |
316
|
|
|
->addNode($relation->to->id, $relation->distance); |
317
|
|
|
} |
318
|
|
|
} |
319
|
|
|
} |
320
|
|
|
} |
321
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.