From 65d76e11ea1ecc19585b2fdb00cd5d4c13cf3a52 Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Sun, 26 Jan 2020 12:06:11 +0100 Subject: [PATCH] continue graph implementation --- Stdlib/Graph/BinaryTree.php | 226 --------------------------- Stdlib/Graph/Graph.php | 231 +++++++++++++++++++++++++-- Stdlib/Graph/Tree.php | 257 ------------------------------- tests/Stdlib/Graph/GraphTest.php | 1 - 4 files changed, 215 insertions(+), 500 deletions(-) delete mode 100644 Stdlib/Graph/BinaryTree.php delete mode 100644 Stdlib/Graph/Tree.php diff --git a/Stdlib/Graph/BinaryTree.php b/Stdlib/Graph/BinaryTree.php deleted file mode 100644 index e615644d8..000000000 --- a/Stdlib/Graph/BinaryTree.php +++ /dev/null @@ -1,226 +0,0 @@ -getNodes())) { - return $list; - } - - $left = $list->getLeft(); - $list->setLeft($list->invert($list->nodes[1])); - $list->setRight($list->invert($left)); - - return $list; - } - - /** - * Get left node of a node. - * - * @param Node $base Tree node - * - * @return null|Node Left node - * - * @since 1.0.0 - */ - public function getLeft(Node $base) : ?Node - { - $neighbors = $base->getNeighbors($base); - - // todo: index can be wrong, see setLeft/setRight - return $neighbors[0] ?? null; - } - - /** - * Get right node of a node. - * - * @param Node $base Tree node - * - * @return null|Node Right node - * - * @since 1.0.0 - */ - public function getRight(Node $base) : ?Node - { - $neighbors = $this->getNeighbors($base); - - // todo: index can be wrong, see setLeft/setRight - return $neighbors[1] ?? null; - } - - /** - * Set left node of node. - * - * @param Node $base Base node - * @param Node $left Left node - * - * @return BinaryTree - * - * @since 1.0.0 - */ - public function setLeft(Node $base, Node $left) : self - { - if ($this->getLeft($base) === null) { - $this->addNodeRelative($base, $left); - // todo: doesn't know that this is left - // todo: maybe need to add numerics to edges? - } else { - // todo: replace node - $a = 2; - } - - return $this; - } - - /** - * Set right node of node. - * - * @param Node $base Base node - * @param Node $right Right node - * - * @return BinaryTree - * - * @since 1.0.0 - */ - public function setRight(Node $base, Node $right) : self - { - if ($this->getRight($base) === null) { - $this->addNodeRelative($base, $right); - // todo: doesn't know that this is right - // todo: maybe need to add numerics to edges? - } else { - // todo: replace node - $a = 2; - } - - return $this; - } - - /** - * Perform action on tree in in-order. - * - * @param Node $node Tree node - * @param \Closure $callback Task to perform on node - * - * @return void - * - * @since 1.0.0 - */ - public function inOrder(Node $node, \Closure $callback) : void - { - $this->inOrder($this->getLeft($node), $callback); - $callback($node); - $this->inOrder($this->getRight($node), $callback); - } - - /** - * Get nodes in vertical order. - * - * @param Node $node Tree node - * @param int $horizontalDistance Horizontal distance - * @param Node[] $order Ordered nodes by horizontal distance - * - * @return void - * - * @since 1.0.0 - */ - private function getVerticalOrder(Node $node, int $horizontalDistance, array &$order) : void - { - if (!isset($order[$horizontalDistance])) { - $order[$horizontalDistance] = []; - } - - $order[$horizontalDistance][] = $node; - $left = $this->getLeft($node); - $right = $this->getRight($node); - - if ($left !== null) { - $this->getVerticalOrder($left, $horizontalDistance - 1, $order); - } - - if ($right !== null) { - $this->getVerticalOrder($right, $horizontalDistance + 1, $order); - } - } - - /** - * Perform action on tree in vertical-order. - * - * @param Node $node Tree node - * @param \Closure $callback Task to perform on node - * - * @return void - * - * @since 1.0.0 - */ - public function verticalOrder(Node $node, \Closure $callback) : void - { - $order = []; - $this->getVerticalOrder($node, 0, $order); - - foreach ($order as $level) { - foreach ($level as $node) { - $callback($node); - } - } - } - - /** - * Check if tree is symmetric. - * - * @param null|Node $node1 Tree node1 - * @param null|Node $node2 Tree node2 (optional, can be different tree) - * - * @return bool True if tree is symmetric, false if tree is not symmetric - * - * @since 1.0.0 - */ - public function isSymmetric(Node $node1 = null, Node $node2 = null) : bool - { - if (($node1 === null && $node2 === null) - || $node1->isEqual($node2) - ) { - return true; - } elseif ($node1 === null || $node2 === null) { - return false; - } - - $left1 = $this->getLeft($node1); - $right1 = $this->getRight($node1); - - $left2 = $node2 !== null ? $this->getLeft($node1) : null; - $right2 = $node2 !== null ? $this->getRight($node1) : null; - - return $this->isSymmetric($left1, $right2) && $this->isSymmetric($right1, $left2); - } -} diff --git a/Stdlib/Graph/Graph.php b/Stdlib/Graph/Graph.php index a4d9763ef..ddb3078fb 100644 --- a/Stdlib/Graph/Graph.php +++ b/Stdlib/Graph/Graph.php @@ -21,6 +21,19 @@ namespace phpOMS\Stdlib\Graph; * @license OMS License 1.0 * @link https://orange-management.org * @since 1.0.0 + * + * @todo Orange-Management/phpOMS#10 + * * Count all paths between 2 nodes + * * Return all paths between 2 nodes + * * Find cycles using graph coloring + * * Find a negative cycle + * * Find cycles with n length + * * Find cycles with odd length + * * Find shortest path between 2 nodes + * * Find longest path between 2 nodes + * * Find islands + * * Find all unreachable nodes + * * Check if strongly connected */ class Graph { @@ -32,6 +45,14 @@ class Graph */ protected $nodes = []; + /** + * Directed + * + * @var bool + * @since 1.0.0 + */ + protected $isDirected = false; + /** * Set node to graph. * @@ -62,6 +83,20 @@ class Graph return $this->nodes[$key] ?? null; } + /** + * Graph has node + * + * @param mixed $key Node key + * + * @return bool + * + * @since 1.0.0 + */ + public function hasNode($key) : bool + { + return isset($this->nodes[$key]); + } + /** * Get graph nodes * @@ -74,6 +109,51 @@ class Graph return $this->nodes; } + /** + * Is directed graph + * + * @return bool + * + * @since 1.0.0 + */ + public function isDirected() : bool + { + return $this->isDirected; + } + + /** + * Set graph directed + * + * @param bool $directed Is directed? + * + * @return void + * + * @since 1.0.0 + */ + public function setDirected(bool $directed) : void + { + $this->isDirected = $directed; + } + + /** + * Get graph/edge costs + * + * @return int|float + * + * @since 1.0.0 + */ + public function getCost() + { + $edges = $this->getEdges(); + $costs = 0; + + foreach ($edges as $edge) { + $costs += $edge->getWeight(); + } + + return $costs; + } + /** * Get graph edges * @@ -180,37 +260,161 @@ class Graph /** * Get minimal spanning tree using Kruskal's algorithm. * - * @return Tree + * @return Graph * * @since 1.0.0 */ - public function getKruskalMinimalSpanningTree() : Tree + public function getKruskalMinimalSpanningTree() : self { - return new Tree(); + $graph = new self(); + $edges = $this->getEdges(); + + \usort($edges, Edge::class . '::compare'); + + foreach ($edges as $edge) { + if ($graph->hasNode($edge->getNode1()->getId()) + && $graph->hasNode($edge->getNode2()->getId()) + ) { + continue; + } + + /** @var Node $node1 */ + $node1 = $graph->hasNode($edge->getNode1()->getId()) ? $graph->getNode($edge->getNode1()->getId()) : clone $edge->getNode1(); + /** @var Node $node2 */ + $node2 = $graph->hasNode($edge->getNode2()->getId()) ? $graph->getNode($edge->getNode2()->getId()) : clone $edge->getNode2(); + + $node1->setNodeRelative($node2); + + if (!$graph->hasNode($edge->getNode1()->getId())) { + $graph->setNode($node1); + } + + if (!$graph->hasNode($edge->getNode2()->getId())) { + $graph->setNode($node2); + } + } + + return $graph; } /** - * Get minimal spanning tree using Prim's algorithm + * Has cycle in graph. * - * @return Tree + * @return bool * * @since 1.0.0 */ - public function getPrimMinimalSpanningTree() : Tree + public function hasCycle() : bool { - return new Tree(); + return $this->isDirected ? $this->hasCycleDirected() : $this->hasCycleUndirected(); } /** - * Get circles in graph. + * Has cycle in directed graph. * - * @return array + * @return bool * * @since 1.0.0 */ - public function getCircle() : array + private function hasCycleDirected() : bool { - return []; + $visited = []; + $recStack = []; + + foreach ($this->nodes as $node) { + if ($this->hasDirectedCyclicUtil($node->getId(), $visited, $recStack)) { + return true; + } + } + + return false; + } + + /** + * Has cycle in directed graph. + * + * @param string $node Node name + * @param array $visited Visited nodes + * @param array $stack Recursion stack + * + * @return bool + * + * @since 1.0.0 + */ + private function hasDirectedCyclicUtil(string $node, array &$visited, array &$stack) : bool + { + if (isset($visited[$node]) && $visited[$node]) { + $stack[$node] = false; + + return false; + } + + $visited[$node] = true; + $stack[$node] = true; + + $neighbors = $this->nodes[$node]->getNeighbors(); + foreach ($neighbors as $neighbor) { + if ((!isset($visited[$neighbor->getId()]) || !$visited[$neighbor->getId()]) + && $this->hasDirectedCyclicUtil($neighbor->getId(), $visited, $stack) + ) { + return true; + } elseif (isset($stack[$neighbor->getId()]) && $stack[$neighbor->getId()]) { + return true; + } + } + + $stack[$node] = false; + + return false; + } + + /** + * Has cycle in undirected graph. + * + * @return bool + * + * @since 1.0.0 + */ + private function hasCycleUndirected() : bool + { + $visited = []; + + foreach ($this->nodes as $node) { + if (!isset($visited[$node->getId()]) && $this->hasUndirectedCyclicUtil($node->getId(), $visited, null)) { + return true; + } + } + + return false; + } + + /** + * Has cycle in undirected graph. + * + * @param string $node Node name + * @param array $visited Visited nodes + * @param null|string $parent Parent node + * + * @return bool + * + * @since 1.0.0 + */ + private function hasUndirectedCyclicUtil(string $node, array &$visited, ?string $parent) : bool + { + $visited[$node] = true; + + $neighbors = $this->nodes[$node]->getNeighbors(); + foreach ($neighbors as $neighbor) { + if (!isset($visited[$neighbor->getId()])) { + if ($this->hasUndirectedCyclicUtil($neighbor->getId(), $visited, $node)) { + return true; + } + } elseif ($neighbor->getId() !== $parent) { + return true; + } + } + + return false; } /** @@ -411,7 +615,6 @@ class Graph */ public function isConnected() : bool { - // todo: implement return true; } @@ -424,7 +627,6 @@ class Graph */ public function getUnconnected() : array { - // todo: implement // get all unconnected sub graphs return []; @@ -439,7 +641,6 @@ class Graph */ public function isBipartite() : bool { - // todo: implement return true; } @@ -452,7 +653,6 @@ class Graph */ public function isTriangleFree() : bool { - // todo: implement return true; } @@ -465,7 +665,6 @@ class Graph */ public function isCircleFree() : bool { - // todo: implement return true; } } diff --git a/Stdlib/Graph/Tree.php b/Stdlib/Graph/Tree.php deleted file mode 100644 index dc67718e4..000000000 --- a/Stdlib/Graph/Tree.php +++ /dev/null @@ -1,257 +0,0 @@ -root; - - if ($currentNode === null) { - return 0; - } - - $depth = 1; - $neighbors = $this->getNeighbors($currentNode); - - foreach ($neighbors as $neighbor) { - $depth = \max($depth, $depth + $this->getMaxDepth($neighbor)); - } - - return $depth; - } - - /** - * Get minimum tree path. - * - * @param Node $node Tree node - * - * @return int - * - * @since 1.0.0 - */ - public function getMinDepth(Node $node = null) : int - { - $currentNode = $node ?? $this->root; - - if ($currentNode === null) { - return 0; - } - - $depth = []; - $neighbors = $this->getNeighbors($currentNode); - - foreach ($neighbors as $neighbor) { - $depth[] = $this->getMaxDepth($neighbor); - } - - $depth = empty($depth) ? 0 : $depth; - - return \min($depth) + 1; - } - - /** - * Perform task on tree nodes in level order. - * - * @param Node $node Tree node - * @param \Closure $callback Task to perform - * - * @return void - * - * @since 1.0.0 - */ - public function levelOrder(Node $node, \Closure $callback) : void - { - $depth = $this->getMaxDepth(); - - for ($i = 1; $i < $depth; ++$i) { - $nodes = $this->getLevel($i); - callback($nodes); - } - } - - /** - * Check if node is leaf. - * - * @param Node $node Tree node - * - * @return bool - * - * @since 1.0.0 - */ - public function isLeaf(Node $node) : bool - { - return \count($this->getEdgesOfNode($node)) === 1; - } - - /** - * Get all nodes of a specific level. - * - * @param int $level Level to retrieve - * @param Node $node Tree node - * - * @return Node[] - * - * @since 1.0.0 - */ - public function getLevelNodes(int $level, Node $node) : array - { - --$level; - $neighbors = $this->getNeighbors($node); - $nodes = []; - - if ($level === 1) { - return $neighbors; - } - - foreach ($neighbors as $neighbor) { - \array_merge($nodes, $this->getLevelNodes($level, $neighbor)); - } - - return $nodes; - } - - /** - * Check if the tree is full. - * - * @param int $type Child nodes per non-leaf node - * - * @return bool - * - * @since 1.0.0 - */ - public function isFull(int $type) : bool - { - if (\count($this->edges) % $type !== 0) { - return false; - } - - foreach ($this->nodes as $node) { - $neighbors = \count($this->getNeighbors($node)); - - if ($neighbors !== $type && $neighbors !== 0) { - return false; - } - } - - return true; - } - - /** - * Perform action on tree in pre-order. - * - * @param Node $node Tree node - * @param \Closure $callback Task to perform on node - * - * @return void - * - * @since 1.0.0 - */ - public function preOrder(Node $node, \Closure $callback) : void - { - if (\count($this->nodes) === 0) { - return; - } - - $callback($node); - $neighbors = $this->getNeighbors($node); - - foreach ($neighbors as $neighbor) { - // todo: get neighbors needs to return in ordered way - $this->preOrder($neighbor, $callback); - } - } - - /** - * Perform action on tree in post-order. - * - * @param Node $node Tree node - * @param \Closure $callback Task to perform on node - * - * @return void - * - * @since 1.0.0 - */ - public function postOrder(Node $node, \Closure $callback) : void - { - if (\count($this->nodes) === 0) { - return; - } - - $neighbors = $this->getNeighbors($node); - - foreach ($neighbors as $neighbor) { - // todo: get neighbors needs to return in ordered way - $this->postOrder($neighbor, $callback); - } - - $callback($node); - } -} diff --git a/tests/Stdlib/Graph/GraphTest.php b/tests/Stdlib/Graph/GraphTest.php index 3f242b9e1..62a58b9f8 100644 --- a/tests/Stdlib/Graph/GraphTest.php +++ b/tests/Stdlib/Graph/GraphTest.php @@ -50,7 +50,6 @@ class GraphTest extends \PHPUnit\Framework\TestCase self::assertTrue($graph->isCircleFree()); self::assertEquals([], $graph->getBridges()); - self::assertEquals([], $graph->getCircle()); self::assertEquals([], $graph->getFloydWarshallShortestPath()); self::assertEquals([], $graph->getDijkstraShortestPath()); self::assertEquals([], $graph->depthFirstTraversal());