Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.85% covered (warning)
87.85%
347 / 395
72.97% covered (warning)
72.97%
27 / 37
CRAP
0.00% covered (danger)
0.00%
0 / 1
Graph
87.85% covered (warning)
87.85%
347 / 395
72.97% covered (warning)
72.97%
27 / 37
273.50
0.00% covered (danger)
0.00%
0 / 1
 setNode
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getNode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setNodeRelative
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasNode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNodes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDirected
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 getCost
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getEdges
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 getEdge
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
9
 getBridges
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 bridgesDepthFirstSearch
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
8
 getKruskalMinimalSpanningTree
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
8
 hasCycle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 hasCycleDirected
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 hasDirectedCyclicUtil
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
9
 hasCycleUndirected
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 hasUndirectedCyclicUtil
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 getFloydWarshallShortestPath
66.67% covered (warning)
66.67%
18 / 27
0.00% covered (danger)
0.00%
0 / 1
23.33
 pathBetweenNodesDfs
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 findAllReachableNodesDfs
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
7
 breadthFirstTraversal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 longestPath
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 longestPathDfs
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 getAllPathsBetweenNodes
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 countAllPathsBetweenNodes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 longestPathBetweenNodes
92.31% covered (success)
92.31%
24 / 26
0.00% covered (danger)
0.00%
0 / 1
11.06
 shortestPathBetweenNodes
92.00% covered (success)
92.00%
23 / 25
0.00% covered (danger)
0.00%
0 / 1
11.06
 getOrder
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSize
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDiameter
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getGirth
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
10
 getCircuitRank
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 cycleDfs
75.00% covered (warning)
75.00%
12 / 16
0.00% covered (danger)
0.00%
0 / 1
10.27
 isConnected
47.73% covered (danger)
47.73%
21 / 44
0.00% covered (danger)
0.00%
0 / 1
70.56
 isStronglyConnected
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
5.07
 isBipartite
80.00% covered (warning)
80.00%
16 / 20
0.00% covered (danger)
0.00%
0 / 1
8.51
 hasTriangles
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
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 */
13declare(strict_types=1);
14
15namespace 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 */
25class 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}