continue graph implementation

This commit is contained in:
Dennis Eichhorn 2020-01-26 12:06:11 +01:00
parent 89f9d0c2f3
commit 65d76e11ea
4 changed files with 215 additions and 500 deletions

View File

@ -1,226 +0,0 @@
<?php
/**
* Orange Management
*
* PHP Version 7.4
*
* @package phpOMS\Stdlib\Graph
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://orange-management.org
*/
declare(strict_types=1);
namespace phpOMS\Stdlib\Graph;
/**
* Tree class.
*
* @package phpOMS\Stdlib\Graph
* @license OMS License 1.0
* @link https://orange-management.org
* @since 1.0.0
*/
class BinaryTree extends Tree
{
/**
* Invert the tree
*
* @return BinaryTree
*
* @since 1.0.0
*/
public static function invert($list) : self
{
if (empty($list->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);
}
}

View File

@ -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;
}
}

View File

@ -1,257 +0,0 @@
<?php
/**
* Orange Management
*
* PHP Version 7.4
*
* @package phpOMS\Stdlib\Graph
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://orange-management.org
*/
declare(strict_types=1);
namespace phpOMS\Stdlib\Graph;
/**
* Tree class.
*
* @package phpOMS\Stdlib\Graph
* @license OMS License 1.0
* @link https://orange-management.org
* @since 1.0.0
*/
class Tree extends Graph
{
/**
* Root node.
*
* @var Node
* @since 1.0.0
*/
private $root = null;
/**
* Constructor.
*
* @since 1.0.0
*/
public function __construct()
{
$root = new Node();
parent::addNode($root);
}
/**
* Add a note relative to a node.
*
* @param Node $base Base node
* @param Node $node Node to add
*
* @return Tree
*
* @since 1.0.0
*/
public function addRelativeNode(Node $base, Node $node) : self
{
parent::addNode($node);
parent::addEdge(new Edge($base, $node));
return $this;
}
/**
* Get maximum tree depth.
*
* @param Node $node Tree node
*
* @return int
*
* @since 1.0.0
*/
public function getMaxDepth(Node $node = null) : int
{
$currentNode = $node ?? $this->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);
}
}

View File

@ -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());