Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
87.85% |
347 / 395 |
|
72.97% |
27 / 37 |
CRAP | |
0.00% |
0 / 1 |
Graph | |
87.85% |
347 / 395 |
|
72.97% |
27 / 37 |
273.50 | |
0.00% |
0 / 1 |
setNode | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getNode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setNodeRelative | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasNode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNodes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isDirected | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
getCost | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getEdges | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
getEdge | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
9 | |||
getBridges | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
bridgesDepthFirstSearch | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
8 | |||
getKruskalMinimalSpanningTree | |
96.00% |
24 / 25 |
|
0.00% |
0 / 1 |
8 | |||
hasCycle | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
hasCycleDirected | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
hasDirectedCyclicUtil | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
9 | |||
hasCycleUndirected | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
hasUndirectedCyclicUtil | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
getFloydWarshallShortestPath | |
66.67% |
18 / 27 |
|
0.00% |
0 / 1 |
23.33 | |||
pathBetweenNodesDfs | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
6 | |||
findAllReachableNodesDfs | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
7 | |||
breadthFirstTraversal | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
longestPath | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
longestPathDfs | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
getAllPathsBetweenNodes | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
5 | |||
countAllPathsBetweenNodes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
longestPathBetweenNodes | |
92.31% |
24 / 26 |
|
0.00% |
0 / 1 |
11.06 | |||
shortestPathBetweenNodes | |
92.00% |
23 / 25 |
|
0.00% |
0 / 1 |
11.06 | |||
getOrder | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSize | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getDiameter | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getGirth | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
10 | |||
getCircuitRank | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
5 | |||
cycleDfs | |
75.00% |
12 / 16 |
|
0.00% |
0 / 1 |
10.27 | |||
isConnected | |
47.73% |
21 / 44 |
|
0.00% |
0 / 1 |
70.56 | |||
isStronglyConnected | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
5.07 | |||
isBipartite | |
80.00% |
16 / 20 |
|
0.00% |
0 / 1 |
8.51 | |||
hasTriangles | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
7.14 |
1 | <?php |
2 | /** |
3 | * Jingga |
4 | * |
5 | * PHP Version 8.1 |
6 | * |
7 | * @package phpOMS\Stdlib\Graph |
8 | * @copyright Dennis Eichhorn |
9 | * @license OMS License 2.0 |
10 | * @version 1.0.0 |
11 | * @link https://jingga.app |
12 | */ |
13 | declare(strict_types=1); |
14 | |
15 | namespace phpOMS\Stdlib\Graph; |
16 | |
17 | /** |
18 | * Graph class. |
19 | * |
20 | * @package phpOMS\Stdlib\Graph |
21 | * @license OMS License 2.0 |
22 | * @link https://jingga.app |
23 | * @since 1.0.0 |
24 | */ |
25 | class Graph |
26 | { |
27 | /** |
28 | * Nodes. |
29 | * |
30 | * @var Node[] |
31 | * @since 1.0.0 |
32 | */ |
33 | protected array $nodes = []; |
34 | |
35 | /** |
36 | * Directed |
37 | * |
38 | * @var bool |
39 | * @since 1.0.0 |
40 | */ |
41 | public bool $isDirected = false; |
42 | |
43 | /** |
44 | * Set node to graph. |
45 | * |
46 | * @param Node $node Graph node |
47 | * |
48 | * @return Graph |
49 | * |
50 | * @since 1.0.0 |
51 | */ |
52 | public function setNode(Node $node) : self |
53 | { |
54 | $this->nodes[$node->getId()] = $node; |
55 | |
56 | return $this; |
57 | } |
58 | |
59 | /** |
60 | * Get graph node |
61 | * |
62 | * @param int|string $key Node key |
63 | * |
64 | * @return null|Node |
65 | * |
66 | * @since 1.0.0 |
67 | */ |
68 | public function getNode(int | string $key) : ?Node |
69 | { |
70 | return $this->nodes[$key] ?? null; |
71 | } |
72 | |
73 | /** |
74 | * Define a relationship between two nodes |
75 | * |
76 | * @param Node $node1 First node |
77 | * @param Node $node2 Second node |
78 | * @param bool $directed Is directed |
79 | * |
80 | * @return Edge |
81 | * |
82 | * @since 1.0.0 |
83 | */ |
84 | public function setNodeRelative(Node $node1, Node $node2, bool $directed = false) : Edge |
85 | { |
86 | return $node1->setNodeRelative($node2, null, $directed); |
87 | } |
88 | |
89 | /** |
90 | * Graph has node |
91 | * |
92 | * @param int|string $key Node key |
93 | * |
94 | * @return bool |
95 | * |
96 | * @since 1.0.0 |
97 | */ |
98 | public function hasNode(int | string $key) : bool |
99 | { |
100 | return isset($this->nodes[$key]); |
101 | } |
102 | |
103 | /** |
104 | * Get graph nodes |
105 | * |
106 | * @return Node[] |
107 | * |
108 | * @since 1.0.0 |
109 | */ |
110 | public function getNodes() : array |
111 | { |
112 | return $this->nodes; |
113 | } |
114 | |
115 | /** |
116 | * Is directed graph |
117 | * |
118 | * @return bool |
119 | * |
120 | * @since 1.0.0 |
121 | */ |
122 | public function isDirected() : bool |
123 | { |
124 | foreach ($this->nodes as $node) { |
125 | $edges = $node->getEdges(); |
126 | foreach ($edges as $edge) { |
127 | if ($edge->isDirected) { |
128 | return true; |
129 | } |
130 | } |
131 | } |
132 | |
133 | return false; |
134 | } |
135 | |
136 | /** |
137 | * Get graph/edge costs |
138 | * |
139 | * @return int|float |
140 | * |
141 | * @since 1.0.0 |
142 | */ |
143 | public function getCost() : int | float |
144 | { |
145 | $edges = $this->getEdges(); |
146 | $costs = 0; |
147 | |
148 | foreach ($edges as $edge) { |
149 | $costs += $edge->weight; |
150 | } |
151 | |
152 | return $costs; |
153 | } |
154 | |
155 | /** |
156 | * Get graph edges |
157 | * |
158 | * @return Edge[] |
159 | * |
160 | * @since 1.0.0 |
161 | */ |
162 | public function getEdges() : array |
163 | { |
164 | $edges = []; |
165 | |
166 | foreach ($this->nodes as $node) { |
167 | $nodeEdges = $node->getEdges(); |
168 | |
169 | foreach ($nodeEdges as $edge) { |
170 | if (!isset($edges[$edge->node1->getId() . ':' . $edge->node2->getId()]) |
171 | && !isset($edges[$edge->node2->getId() . ':' . $edge->node1->getId()]) |
172 | ) { |
173 | $edges[$edge->node1->getId() . ':' . $edge->node2->getId()] = $edge; |
174 | } |
175 | } |
176 | } |
177 | |
178 | return $edges; |
179 | } |
180 | |
181 | /** |
182 | * Get the edge between two nodes |
183 | * |
184 | * @param string $id1 Node id |
185 | * @param string $id2 Node id |
186 | * |
187 | * @return null|Edge |
188 | * |
189 | * @since 1.0.0 |
190 | */ |
191 | public function getEdge(string $id1, string $id2) : ?Edge |
192 | { |
193 | foreach ($this->nodes as $node) { |
194 | if ($node->getId() !== $id1 && $node->getId() !== $id2) { |
195 | continue; |
196 | } |
197 | |
198 | $nodeEdges = $node->getEdges(); |
199 | |
200 | foreach ($nodeEdges as $edge) { |
201 | if (($edge->node1->getId() === $id1 || $edge->node1->getId() === $id2) |
202 | && ($edge->node2->getId() === $id1 || $edge->node2->getId() === $id2) |
203 | ) { |
204 | return $edge; |
205 | } |
206 | } |
207 | } |
208 | |
209 | return null; |
210 | } |
211 | |
212 | /** |
213 | * Get all bridges. |
214 | * |
215 | * @return Edge[] |
216 | * |
217 | * @since 1.0.0 |
218 | */ |
219 | public function getBridges() : array |
220 | { |
221 | $visited = []; |
222 | $parent = []; |
223 | $discovery = []; |
224 | $low = []; |
225 | $index = 0; |
226 | $bridges = []; |
227 | |
228 | foreach ($this->nodes as $i => $node) { |
229 | if (!isset($visited[$i]) || $visited[$i] === false) { |
230 | $this->bridgesDepthFirstSearch($node, $visited, $discovery, $low, $parent, $index, $bridges); |
231 | } |
232 | } |
233 | |
234 | return $bridges; |
235 | } |
236 | |
237 | /** |
238 | * Fill bridge array |
239 | * |
240 | * @param Node $node Node to check bridge for |
241 | * @param bool[] $visited Visited nodes |
242 | * @param int[] $discovery Discovered |
243 | * @param int[] $low Lowest preorder of any vertex connected to ? |
244 | * @param Node[] $parent Parent node |
245 | * @param int $index Node index |
246 | * @param Edge[] $bridges Edges which represent bridges |
247 | * |
248 | * @return void |
249 | * |
250 | * @since 1.0.0 |
251 | */ |
252 | private function bridgesDepthFirstSearch( |
253 | Node $node, |
254 | array &$visited, |
255 | array &$discovery, |
256 | array &$low, |
257 | array &$parent, |
258 | int &$index, |
259 | array &$bridges |
260 | ) : void |
261 | { |
262 | $id = $node->getId(); |
263 | $visited[$id] = true; |
264 | |
265 | ++$index; |
266 | |
267 | $discovery[$id] = $index; |
268 | $low[$id] = $index; |
269 | |
270 | $edges = $this->nodes[$id]->getEdges(); |
271 | foreach ($edges as $edge) { |
272 | $neighbor = $edge->node1->isEqual($node) ? $edge->node2 : $edge->node1; |
273 | |
274 | if (!isset($visited[$neighbor->getId()]) || !$visited[$neighbor->getId()]) { |
275 | $parent[$neighbor->getId()] = $node; |
276 | |
277 | $this->bridgesDepthFirstSearch($neighbor, $visited, $discovery, $low, $parent, $index, $bridges); |
278 | |
279 | $low[$id] = \min($low[$id], $low[$neighbor->getId()]); |
280 | |
281 | if ($low[$neighbor->getId()] > $discovery[$id]) { |
282 | $bridges[] = $edge; |
283 | } |
284 | } elseif (isset($parent[$id]) && !$neighbor->isEqual($parent[$id])) { |
285 | $low[$id] = \min($low[$id], $discovery[$neighbor->getId()]); |
286 | } |
287 | } |
288 | } |
289 | |
290 | /** |
291 | * Get minimal spanning tree using Kruskal's algorithm. |
292 | * |
293 | * @return Graph |
294 | * |
295 | * @since 1.0.0 |
296 | */ |
297 | public function getKruskalMinimalSpanningTree() : self |
298 | { |
299 | $graph = new self(); |
300 | $edges = $this->getEdges(); |
301 | |
302 | \usort($edges, Edge::class . '::compare'); |
303 | |
304 | foreach ($edges as $edge) { |
305 | if ($graph->hasNode($edge->node1->getId()) |
306 | && $graph->hasNode($edge->node2->getId()) |
307 | ) { |
308 | continue; |
309 | } |
310 | |
311 | /** @var Node $node1 */ |
312 | $node1 = $graph->hasNode($edge->node1->getId()) |
313 | ? $graph->getNode($edge->node1->getId()) |
314 | : clone $edge->node1; |
315 | |
316 | /** @var Node $node2 */ |
317 | $node2 = $graph->hasNode($edge->node2->getId()) |
318 | ? $graph->getNode($edge->node2->getId()) |
319 | : clone $edge->node2; |
320 | |
321 | if (!$graph->hasNode($node1->getId())) { |
322 | $node1->removeEdges(); |
323 | $graph->setNode($node1); |
324 | } |
325 | |
326 | if (!$graph->hasNode($node2->getId())) { |
327 | $node2->removeEdges(); |
328 | $graph->setNode($node2); |
329 | } |
330 | |
331 | $node1->setNodeRelative($node2) |
332 | ->weight = $this->getEdge( |
333 | $node1->getId(), |
334 | $node2->getId() |
335 | )?->weight ?? 0.0; |
336 | } |
337 | |
338 | return $graph; |
339 | } |
340 | |
341 | /** |
342 | * Has cycle in graph. |
343 | * |
344 | * @return bool |
345 | * |
346 | * @since 1.0.0 |
347 | */ |
348 | public function hasCycle() : bool |
349 | { |
350 | return $this->isDirected ? $this->hasCycleDirected() : $this->hasCycleUndirected(); |
351 | } |
352 | |
353 | /** |
354 | * Has cycle in directed graph. |
355 | * |
356 | * @return bool |
357 | * |
358 | * @since 1.0.0 |
359 | */ |
360 | private function hasCycleDirected() : bool |
361 | { |
362 | $visited = []; |
363 | $recStack = []; |
364 | |
365 | foreach ($this->nodes as $node) { |
366 | if ($this->hasDirectedCyclicUtil($node->getId(), $visited, $recStack)) { |
367 | return true; |
368 | } |
369 | } |
370 | |
371 | return false; |
372 | } |
373 | |
374 | /** |
375 | * Has cycle in directed graph. |
376 | * |
377 | * @param string $node Node name |
378 | * @param array $visited Visited nodes |
379 | * @param array $stack Recursion stack |
380 | * |
381 | * @return bool |
382 | * |
383 | * @since 1.0.0 |
384 | */ |
385 | private function hasDirectedCyclicUtil(string $node, array &$visited, array &$stack) : bool |
386 | { |
387 | if (isset($visited[$node]) && $visited[$node]) { |
388 | $stack[$node] = false; |
389 | |
390 | return false; |
391 | } |
392 | |
393 | $visited[$node] = true; |
394 | $stack[$node] = true; |
395 | |
396 | $neighbors = $this->nodes[$node]->getNeighbors(); |
397 | foreach ($neighbors as $neighbor) { |
398 | if ((!isset($visited[$neighbor->getId()]) || !$visited[$neighbor->getId()]) |
399 | && $this->hasDirectedCyclicUtil($neighbor->getId(), $visited, $stack) |
400 | ) { |
401 | return true; |
402 | } elseif (isset($stack[$neighbor->getId()]) && $stack[$neighbor->getId()]) { |
403 | return true; |
404 | } |
405 | } |
406 | |
407 | $stack[$node] = false; |
408 | |
409 | return false; |
410 | } |
411 | |
412 | /** |
413 | * Has cycle in undirected graph. |
414 | * |
415 | * @return bool |
416 | * |
417 | * @since 1.0.0 |
418 | */ |
419 | private function hasCycleUndirected() : bool |
420 | { |
421 | $visited = []; |
422 | |
423 | foreach ($this->nodes as $node) { |
424 | if (!isset($visited[$node->getId()]) && $this->hasUndirectedCyclicUtil($node->getId(), $visited, null)) { |
425 | return true; |
426 | } |
427 | } |
428 | |
429 | return false; |
430 | } |
431 | |
432 | /** |
433 | * Has cycle in undirected graph. |
434 | * |
435 | * @param string $node Node name |
436 | * @param array $visited Visited nodes |
437 | * @param null|string $parent Parent node |
438 | * |
439 | * @return bool |
440 | * |
441 | * @since 1.0.0 |
442 | */ |
443 | private function hasUndirectedCyclicUtil(string $node, array &$visited, ?string $parent) : bool |
444 | { |
445 | $visited[$node] = true; |
446 | |
447 | $neighbors = $this->nodes[$node]->getNeighbors(); |
448 | foreach ($neighbors as $neighbor) { |
449 | if (!isset($visited[$neighbor->getId()])) { |
450 | if ($this->hasUndirectedCyclicUtil($neighbor->getId(), $visited, $node)) { |
451 | return true; |
452 | } |
453 | } elseif ($neighbor->getId() !== $parent) { |
454 | return true; |
455 | } |
456 | } |
457 | |
458 | return false; |
459 | } |
460 | |
461 | /** |
462 | * Get shortest path using Floyd Warschall algorithm. |
463 | * |
464 | * @return array |
465 | * |
466 | * @since 1.0.0 |
467 | */ |
468 | public function getFloydWarshallShortestPath() : array |
469 | { |
470 | $distances = []; |
471 | foreach ($this->nodes as $k) { |
472 | foreach ($this->nodes as $i) { |
473 | foreach ($this->nodes as $j) { |
474 | if (!isset($distances[$i->getId()])) { |
475 | $distances[$i->getId()] = []; |
476 | |
477 | foreach ($this->nodes as $node) { |
478 | if ($i->isEqual($node)) { |
479 | $distances[$i->getId()][$node->getId()] = 0; |
480 | } else { |
481 | $edge = $i->getEdgeByNeighbor($node); |
482 | |
483 | $distances[$i->getId()][$node->getId()] = $edge === null |
484 | ? null |
485 | : $edge->weight; |
486 | } |
487 | } |
488 | } |
489 | |
490 | if (!isset($distances[$k->getId()])) { |
491 | $distances[$k->getId()] = []; |
492 | |
493 | foreach ($this->nodes as $node) { |
494 | if ($k->isEqual($node)) { |
495 | $distances[$k->getId()][$node->getId()] = 0; |
496 | } else { |
497 | $edge = $k->getEdgeByNeighbor($node); |
498 | |
499 | $distances[$k->getId()][$node->getId()] = $edge === null |
500 | ? null |
501 | : $edge->weight; |
502 | } |
503 | } |
504 | } |
505 | |
506 | if ($distances[$i->getId()][$k->getId()] !== null |
507 | && $distances[$k->getId()][$j->getId()] !== null |
508 | && $distances[$i->getId()][$j->getId()] > $distances[$i->getId()][$k->getId()] + $distances[$k->getId()][$j->getId()] |
509 | ) { |
510 | $distances[$i->getId()][$j->getId()] = $distances[$i->getId()][$k->getId()] + $distances[$k->getId()][$j->getId()]; |
511 | } |
512 | } |
513 | } |
514 | } |
515 | |
516 | return $distances; |
517 | } |
518 | |
519 | /** |
520 | * Perform depth first traversal |
521 | * |
522 | * @param Node $node1 Graph node |
523 | * @param Node $node2 Graph node |
524 | * @param array $visited Is the node already visited |
525 | * @param array $path Array of nodes (a path through the graph = connected nodes) |
526 | * @param array $paths Array of paths (all identified paths) |
527 | * |
528 | * @return void |
529 | * |
530 | * @since 1.0.0 |
531 | */ |
532 | private function pathBetweenNodesDfs( |
533 | Node $node1, |
534 | Node $node2 = null, |
535 | array &$visited, |
536 | array &$path, |
537 | array &$paths |
538 | ) : void |
539 | { |
540 | $visited[$node1->getId()] = true; |
541 | |
542 | if ($node2 !== null && $node1->isEqual($node2)) { |
543 | $paths[] = $path; |
544 | } |
545 | |
546 | $neighbors = $node1->getNeighbors(); |
547 | foreach ($neighbors as $neighbor) { |
548 | if (!isset($visited[$neighbor->getId()]) || !$visited[$neighbor->getId()]) { |
549 | $path[] = $neighbor; |
550 | $this->pathBetweenNodesDfs($neighbor, $node2, $visited, $path, $paths); |
551 | \array_pop($path); |
552 | } |
553 | } |
554 | |
555 | $visited[$node1->getId()] = false; |
556 | } |
557 | |
558 | /** |
559 | * Find all reachable nodes with depth first traversal (iterative) |
560 | * |
561 | * Includes the node itself |
562 | * |
563 | * @param int|string|Node $node1 Graph node |
564 | * |
565 | * @return Node[] |
566 | * |
567 | * @since 1.0.0 |
568 | */ |
569 | public function findAllReachableNodesDfs(int | string | Node $node1) : array |
570 | { |
571 | $nodes = []; |
572 | |
573 | if (!($node1 instanceof Node)) { |
574 | $node1 = $this->getNode($node1); |
575 | } |
576 | |
577 | if ($node1 === null) { |
578 | return $nodes; |
579 | } |
580 | |
581 | $visited = []; |
582 | $stack = [$node1]; |
583 | |
584 | while (!empty($stack)) { |
585 | $cNode = \array_pop($stack); |
586 | if (isset($visited[$cNode->getId()])) { |
587 | continue; |
588 | } |
589 | |
590 | $nodes[] = $cNode; |
591 | $visited[$cNode->getId()] = true; |
592 | |
593 | $neighbors = $cNode->getNeighbors(); |
594 | foreach ($neighbors as $neighbor) { |
595 | if (!isset($visited[$neighbor->getId()])) { |
596 | $stack[] = $neighbor; |
597 | } |
598 | } |
599 | } |
600 | |
601 | return $nodes; |
602 | } |
603 | |
604 | /** |
605 | * Perform breadth first traversal. |
606 | * |
607 | * @return array |
608 | * |
609 | * @since 1.0.0 |
610 | */ |
611 | public function breadthFirstTraversal() : array |
612 | { |
613 | return []; |
614 | } |
615 | |
616 | /** |
617 | * Get longest path in graph. |
618 | * |
619 | * @return Node[] |
620 | * |
621 | * @since 1.0.0 |
622 | */ |
623 | public function longestPath() : array |
624 | { |
625 | $longestPath = []; |
626 | $visited = []; |
627 | $path = []; |
628 | |
629 | foreach ($this->nodes as $node) { |
630 | $visited[$node->getId()] = false; |
631 | } |
632 | |
633 | foreach ($this->nodes as $node) { |
634 | $this->longestPathDfs($node, $visited, $path, $longestPath); |
635 | } |
636 | |
637 | return $longestPath; |
638 | } |
639 | |
640 | /** |
641 | * Perform depth first traversal |
642 | * |
643 | * @param Node $node Graph node |
644 | * @param array $visited Is the node already visited |
645 | * @param array $path Array of nodes (a path through the graph = connected nodes) |
646 | * @param array $longestPath Array of nodes (longest path through the graph = connected nodes) |
647 | * |
648 | * @return void |
649 | * |
650 | * @since 1.0.0 |
651 | */ |
652 | private function longestPathDfs(Node $node, &$visited, &$path, &$longestPath) : void |
653 | { |
654 | $visited[$node->getId()] = true; |
655 | $path[] = $node; |
656 | |
657 | $edges = $node->getEdges(); |
658 | foreach ($edges as $edge) { |
659 | $adj = $edge->node1->isEqual($node) |
660 | ? $edge->node2 |
661 | : $edge->node1; |
662 | |
663 | if (!$visited[$adj->getId()]) { |
664 | $this->longestPathDfs($adj, $visited, $path, $longestPath); |
665 | } |
666 | } |
667 | |
668 | if (\count($path) > \count($longestPath)) { |
669 | $longestPath = $path; |
670 | } |
671 | |
672 | \array_pop($path); |
673 | $visited[$node->getId()] = false; |
674 | } |
675 | |
676 | /** |
677 | * Get all paths between two nodes |
678 | * Inclides end node, but not start node in the paths |
679 | * |
680 | * @param int|string|Node $node1 Graph node |
681 | * @param int|string|Node $node2 Graph node |
682 | * |
683 | * @return array<int, Node[]> |
684 | * |
685 | * @since 1.0.0 |
686 | */ |
687 | public function getAllPathsBetweenNodes(int | string | Node $node1, int | string | Node $node2) : array |
688 | { |
689 | if (!($node1 instanceof Node)) { |
690 | $node1 = $this->getNode($node1); |
691 | } |
692 | |
693 | if (!($node2 instanceof Node)) { |
694 | $node2 = $this->getNode($node2); |
695 | } |
696 | |
697 | if ($node1 === null || $node2 === null) { |
698 | return []; |
699 | } |
700 | |
701 | $path = []; |
702 | $paths = []; |
703 | $visited = []; |
704 | |
705 | $this->pathBetweenNodesDfs($node1, $node2, $visited, $path, $paths); |
706 | |
707 | return $paths; |
708 | } |
709 | |
710 | /** |
711 | * Count possible paths between two nodes. |
712 | * |
713 | * @param int|string|Node $node1 Graph node |
714 | * @param int|string|Node $node2 Graph node |
715 | * |
716 | * @return int |
717 | * |
718 | * @since 1.0.0 |
719 | */ |
720 | public function countAllPathsBetweenNodes(int | string | Node $node1, int | string | Node $node2) : int |
721 | { |
722 | return \count($this->getAllPathsBetweenNodes($node1, $node2)); |
723 | } |
724 | |
725 | /** |
726 | * Get longest path between two nodes. |
727 | * |
728 | * @param int|string|Node $node1 Graph node |
729 | * @param int|string|Node $node2 Graph node |
730 | * |
731 | * @return Node[] |
732 | * |
733 | * @since 1.0.0 |
734 | */ |
735 | public function longestPathBetweenNodes(int | string | Node $node1, int | string | Node $node2) : array |
736 | { |
737 | if (!($node1 instanceof Node)) { |
738 | $node1 = $this->getNode($node1); |
739 | } |
740 | |
741 | if (!($node2 instanceof Node)) { |
742 | $node2 = $this->getNode($node2); |
743 | } |
744 | |
745 | if ($node1 === null || $node2 === null) { |
746 | return []; |
747 | } |
748 | |
749 | $paths = $this->getAllPathsBetweenNodes($node1, $node2); |
750 | |
751 | if (empty($paths)) { |
752 | return []; |
753 | } |
754 | |
755 | $mostNodes = 0; |
756 | $mostNodesCount = 0; |
757 | foreach ($paths as $key => $path) { |
758 | $edges[$key] = 0; |
759 | $nodeCount = 0; |
760 | foreach ($path as $node) { |
761 | if ($node1->getEdgeByNeighbor($node) === null) { |
762 | continue; |
763 | } |
764 | |
765 | $edges[$key] += $node1->getEdgeByNeighbor($node)->weight; |
766 | ++$nodeCount; |
767 | } |
768 | |
769 | if ($nodeCount > $mostNodesCount) { |
770 | $mostNodesCount = $nodeCount; |
771 | $mostNodes = $key; |
772 | } |
773 | } |
774 | |
775 | if (\array_sum($edges) > 0.0) { |
776 | \arsort($edges); |
777 | |
778 | return $paths[\array_key_first($edges)]; |
779 | } |
780 | |
781 | return $paths[$mostNodes]; |
782 | } |
783 | |
784 | /** |
785 | * Get shortest path between two nodes. |
786 | * |
787 | * @param int|string|Node $node1 Graph node |
788 | * @param int|string|Node $node2 Graph node |
789 | * |
790 | * @return Node[] |
791 | * |
792 | * @since 1.0.0 |
793 | */ |
794 | public function shortestPathBetweenNodes(int | string | Node $node1, int | string | Node $node2) : array |
795 | { |
796 | if (!($node1 instanceof Node)) { |
797 | $node1 = $this->getNode($node1); |
798 | } |
799 | |
800 | if (!($node2 instanceof Node)) { |
801 | $node2 = $this->getNode($node2); |
802 | } |
803 | |
804 | if ($node1 === null || $node2 === null) { |
805 | return []; |
806 | } |
807 | |
808 | $paths = $this->getAllPathsBetweenNodes($node1, $node2); |
809 | |
810 | $edges = []; |
811 | $leastNodes = 0; |
812 | $leastNodesCount = \PHP_INT_MAX; |
813 | foreach ($paths as $key => $path) { |
814 | $edges[$key] = 0; |
815 | $nodeCount = 0; |
816 | foreach ($path as $node) { |
817 | if ($node1->getEdgeByNeighbor($node) === null) { |
818 | continue; |
819 | } |
820 | |
821 | $edges[$key] += $node1->getEdgeByNeighbor($node)->weight; |
822 | ++$nodeCount; |
823 | } |
824 | |
825 | if ($nodeCount < $leastNodesCount && $nodeCount > 0) { |
826 | $leastNodesCount = $nodeCount; |
827 | $leastNodes = $key; |
828 | } |
829 | } |
830 | |
831 | if (\array_sum($edges) > 0.0) { |
832 | \asort($edges); |
833 | |
834 | return $paths[\array_key_first($edges)]; |
835 | } |
836 | |
837 | return $paths[$leastNodes]; |
838 | } |
839 | |
840 | /** |
841 | * Get order of the graph. |
842 | * |
843 | * The order of a graph is the amount of nodes it contains. |
844 | * |
845 | * @return int |
846 | * |
847 | * @since 1.0.0 |
848 | */ |
849 | public function getOrder() : int |
850 | { |
851 | return \count($this->nodes); |
852 | } |
853 | |
854 | /** |
855 | * Get size of the graph. |
856 | * |
857 | * The size of the graph is the amount of edges it contains. |
858 | * |
859 | * @return int |
860 | * |
861 | * @since 1.0.0 |
862 | */ |
863 | public function getSize() : int |
864 | { |
865 | $edges = $this->getEdges(); |
866 | |
867 | return \count($edges); |
868 | } |
869 | |
870 | /** |
871 | * Get diameter of graph. |
872 | * |
873 | * The diameter of a graph is the longest shortest path between two nodes. |
874 | * |
875 | * @return int |
876 | * |
877 | * @since 1.0.0 |
878 | */ |
879 | public function getDiameter() : int |
880 | { |
881 | $paths = $this->getFloydWarshallShortestPath(); |
882 | $count = []; |
883 | |
884 | if (empty($paths)) { |
885 | return 0; |
886 | } |
887 | |
888 | foreach ($paths as $path) { |
889 | $count[] = \count($path); |
890 | } |
891 | |
892 | return \max($count); |
893 | } |
894 | |
895 | /** |
896 | * Get the graph girth |
897 | * |
898 | * @return int |
899 | * |
900 | * @since 1.0.0 |
901 | */ |
902 | public function getGirth() : int |
903 | { |
904 | $girth = \PHP_INT_MAX; |
905 | |
906 | foreach ($this->nodes as $i) { |
907 | $stack = [$i]; |
908 | |
909 | foreach ($this->nodes as $node) { |
910 | $distances[$node->getId()] = \PHP_INT_MAX; |
911 | } |
912 | |
913 | $distances[$i->getId()] = 0; |
914 | |
915 | while (!empty($stack)) { |
916 | $current = \array_shift($stack); |
917 | |
918 | foreach ($this->nodes as $j) { |
919 | // Has neighbour |
920 | if ($this->nodes[$current->getId()]->hasNeighbor($j)) { |
921 | if ($distances[$j->getId()] === \PHP_INT_MAX) { |
922 | $distances[$j->getId()] = $distances[$current->getId()] + 1; |
923 | $stack[] = $j; |
924 | } elseif (!$j->isEqual($i) && !$j->isEqual($current) |
925 | && $distances[$j->getId()] >= $distances[$current->getId()] |
926 | ) { |
927 | $girth = (int) \min( |
928 | $girth, |
929 | $distances[$current->getId()] + $distances[$j->getId()] + 1 |
930 | ); |
931 | } |
932 | } |
933 | } |
934 | } |
935 | } |
936 | |
937 | return $girth; |
938 | } |
939 | |
940 | /** |
941 | * Get the graph circuit rank |
942 | * |
943 | * @return int |
944 | * |
945 | * @since 1.0.0 |
946 | */ |
947 | public function getCircuitRank() : int |
948 | { |
949 | $adjMatrix = []; |
950 | foreach ($this->nodes as $node) { |
951 | $adjMatrix[$node->getId()] = $node->getEdges(); |
952 | } |
953 | |
954 | $rank = 0; |
955 | foreach ($this->nodes as $i) { |
956 | $visited = []; |
957 | foreach ($this->nodes as $node) { |
958 | $visited[$node->getId()] = false; |
959 | } |
960 | |
961 | if ($this->cycleDfs($adjMatrix, $i, null, $visited)) { |
962 | ++$rank; |
963 | } |
964 | } |
965 | |
966 | return $rank; |
967 | } |
968 | |
969 | /** |
970 | * Get the graph circuit rank |
971 | * |
972 | * @param array $adjMatrix Adjacency matrix |
973 | * @param Node $current Current node |
974 | * @param Node $previous Previous node |
975 | * @param array $visited Visited nodes |
976 | * |
977 | * @return bool |
978 | * |
979 | * @since 1.0.0 |
980 | */ |
981 | private function cycleDfs(array $adjMatrix, Node $current, ?Node $previous, &$visited) : bool |
982 | { |
983 | $visited[$current->getId()] = true; |
984 | foreach ($adjMatrix[$current->getId()] as $edge) { |
985 | if ($edge->isDirected) { |
986 | if ($edge->node2->isEqual(($current))) { |
987 | continue; |
988 | } |
989 | |
990 | $next = $edge->node2; |
991 | } else { |
992 | $next = $edge->node1->isEqual($current) |
993 | ? $edge->node2 |
994 | : $edge->node1; |
995 | } |
996 | |
997 | if ($previous !== null && $next->isEqual($previous)) { |
998 | continue; |
999 | } |
1000 | |
1001 | if ($visited[$next->getId()]) { |
1002 | return true; |
1003 | } |
1004 | |
1005 | if ($this->cycleDfs($adjMatrix, $next, $current, $visited)) { |
1006 | return true; |
1007 | } |
1008 | } |
1009 | |
1010 | return false; |
1011 | } |
1012 | |
1013 | /** |
1014 | * Is the graph connected? |
1015 | * |
1016 | * @return bool |
1017 | * |
1018 | * @since 1.0.0 |
1019 | */ |
1020 | public function isConnected(int | string | Node $node1 = null, int | string | Node $node2 = null) : bool |
1021 | { |
1022 | if (empty($this->nodes)) { |
1023 | return true; |
1024 | } |
1025 | |
1026 | if ($node1 === null || $node2 === null) { |
1027 | $visited = []; |
1028 | foreach ($this->nodes as $node) { |
1029 | $visited[] = false; |
1030 | } |
1031 | |
1032 | $node = \reset($this->nodes); |
1033 | $visited[$node->getId()] = true; |
1034 | $stack = [$node]; |
1035 | |
1036 | while (!empty($stack)) { |
1037 | $node = \array_pop($stack); |
1038 | |
1039 | $edges = $node->getEdges(); |
1040 | foreach ($edges as $edge) { |
1041 | if ($edge->isDirected) { |
1042 | if ($edge->node2->isEqual(($node))) { |
1043 | continue; |
1044 | } |
1045 | |
1046 | $adj = $edge->node2; |
1047 | } else { |
1048 | $adj = $edge->node1->isEqual($node) |
1049 | ? $edge->node2 |
1050 | : $edge->node1; |
1051 | } |
1052 | |
1053 | if (!$visited[$adj->getId()]) { |
1054 | $visited[$adj->getId()] = true; |
1055 | |
1056 | $stack[] = $adj; |
1057 | } |
1058 | } |
1059 | } |
1060 | |
1061 | return !\in_array(false, $visited); |
1062 | } |
1063 | |
1064 | $visited = []; |
1065 | foreach ($this->nodes as $node) { |
1066 | $visited[$node->getId()] = false; |
1067 | } |
1068 | |
1069 | $stack = [$node1]; |
1070 | while (!empty($stack)) { |
1071 | $node = \array_shift($stack); |
1072 | |
1073 | if ($node->isEqual($node2)) { |
1074 | return true; |
1075 | } |
1076 | |
1077 | if (!$visited[$node->getId()]) { |
1078 | $visited[$node->getId()] = true; |
1079 | |
1080 | $edges = $node->getEdges(); |
1081 | foreach ($edges as $edge) { |
1082 | if ($edge->isDirected) { |
1083 | if ($edge->node2->isEqual(($node))) { |
1084 | continue; |
1085 | } |
1086 | |
1087 | $stack[] = $edge->node2; |
1088 | } else { |
1089 | $stack = $edge->node1->isEqual($node) |
1090 | ? $edge->node2 |
1091 | : $edge->node1; |
1092 | } |
1093 | } |
1094 | } |
1095 | } |
1096 | |
1097 | return false; |
1098 | } |
1099 | |
1100 | /** |
1101 | * Is the graph strongly connected? |
1102 | * |
1103 | * @return bool |
1104 | * |
1105 | * @since 1.0.0 |
1106 | */ |
1107 | public function isStronglyConnected() : bool |
1108 | { |
1109 | if (empty($this->nodes)) { |
1110 | return true; |
1111 | } |
1112 | |
1113 | foreach ($this->nodes as $node1) { |
1114 | foreach ($this->nodes as $node2) { |
1115 | if (!$node1->hasNeighbor($node2)) { |
1116 | return false; |
1117 | } |
1118 | } |
1119 | } |
1120 | |
1121 | return true; |
1122 | } |
1123 | |
1124 | /** |
1125 | * Is the graph bipartite? |
1126 | * |
1127 | * @return bool |
1128 | * |
1129 | * @since 1.0.0 |
1130 | */ |
1131 | public function isBipartite() : bool |
1132 | { |
1133 | if (empty($this->nodes)) { |
1134 | return true; |
1135 | } |
1136 | |
1137 | foreach ($this->nodes as $node) { |
1138 | $colors[$node->getId()] = 0; |
1139 | } |
1140 | |
1141 | $node1 = \reset($this->nodes); |
1142 | $colors[$node1->getId()] = 1; |
1143 | |
1144 | $stack = [$node1]; |
1145 | |
1146 | while (!empty($stack)) { |
1147 | $node = \array_pop($stack); |
1148 | |
1149 | $edges = $node->getEdges(); |
1150 | foreach ($edges as $edge) { |
1151 | $adj = $edge->node1->isEqual($node) |
1152 | ? $edge->node2 |
1153 | : $edge->node1; |
1154 | |
1155 | if ($colors[$adj->getId()] === -1) { |
1156 | $colors[$adj->getId()] = 1 - $colors[$node->getId()]; |
1157 | $stack[] = $adj; |
1158 | } elseif ($colors[$adj->getId()] === $colors[$node->getId()]) { |
1159 | return false; |
1160 | } |
1161 | } |
1162 | } |
1163 | |
1164 | return true; |
1165 | } |
1166 | |
1167 | /** |
1168 | * Is the graph triangle? |
1169 | * |
1170 | * @return bool |
1171 | * |
1172 | * @since 1.0.0 |
1173 | */ |
1174 | public function hasTriangles() : bool |
1175 | { |
1176 | foreach ($this->nodes as $i) { |
1177 | foreach ($this->nodes as $j) { |
1178 | if ($this->getEdge($i->getId(), $j->getId()) !== null) { |
1179 | foreach ($this->nodes as $k) { |
1180 | if ($this->getEdge($j->getId(), $k->getId()) && $this->getEdge($k->getId(), $i->getId())) { |
1181 | return true; |
1182 | } |
1183 | } |
1184 | } |
1185 | } |
1186 | } |
1187 | |
1188 | return false; |
1189 | } |
1190 | } |