Graph.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Framework\Data;
  7. /**
  8. * Graph data structure
  9. */
  10. class Graph
  11. {
  12. /**#@+
  13. * Search modes
  14. */
  15. const DIRECTIONAL = 1;
  16. const INVERSE = 2;
  17. const NON_DIRECTIONAL = 3;
  18. /**#@-*/
  19. /**#@-*/
  20. protected $_nodes = [];
  21. /**
  22. * Declared relations directed "from" "to"
  23. *
  24. * @var array
  25. */
  26. protected $_from = [];
  27. /**
  28. * Inverse relations "to" "from"
  29. *
  30. * @var array
  31. */
  32. protected $_to = [];
  33. /**
  34. * Validate consistency of the declared structure and assign it to the object state
  35. *
  36. * @param array $nodes plain array with node identifiers
  37. * @param array $relations array of 2-item plain arrays, which represent relations of nodes "from" "to"
  38. */
  39. public function __construct(array $nodes, array $relations)
  40. {
  41. foreach ($nodes as $node) {
  42. $this->_assertNode($node, false);
  43. $this->_nodes[$node] = $node;
  44. }
  45. foreach ($relations as $pair) {
  46. list($fromNode, $toNode) = $pair;
  47. $this->addRelation($fromNode, $toNode);
  48. }
  49. }
  50. /**
  51. * Set a relation between nodes
  52. *
  53. * @param string|int $fromNode
  54. * @param string|int $toNode
  55. * @return $this
  56. * @throws \InvalidArgumentException
  57. */
  58. public function addRelation($fromNode, $toNode)
  59. {
  60. if ($fromNode == $toNode) {
  61. throw new \InvalidArgumentException("Graph node '{$fromNode}' is linked to itself.");
  62. }
  63. $this->_assertNode($fromNode, true);
  64. $this->_assertNode($toNode, true);
  65. $this->_from[$fromNode][$toNode] = $toNode;
  66. $this->_to[$toNode][$fromNode] = $fromNode;
  67. return $this;
  68. }
  69. /**
  70. * Export relations between nodes. Can return inverse relations
  71. *
  72. * @param int $mode
  73. * @return array
  74. * @throws \InvalidArgumentException
  75. */
  76. public function getRelations($mode = self::DIRECTIONAL)
  77. {
  78. switch ($mode) {
  79. case self::DIRECTIONAL:
  80. return $this->_from;
  81. case self::INVERSE:
  82. return $this->_to;
  83. case self::NON_DIRECTIONAL:
  84. $graph = $this->_from;
  85. foreach ($this->_to as $idTo => $relations) {
  86. foreach ($relations as $idFrom) {
  87. $graph[$idTo][$idFrom] = $idFrom;
  88. }
  89. }
  90. return $graph;
  91. default:
  92. throw new \InvalidArgumentException("Unknown search mode: '{$mode}'");
  93. }
  94. }
  95. /**
  96. * Find a cycle in the graph
  97. *
  98. * Returns first/all found cycle
  99. * Optionally may specify a node to return a cycle if it is in any
  100. *
  101. * @param string|int $node
  102. * @param boolean $firstOnly found only first cycle
  103. * @return array
  104. */
  105. public function findCycle($node = null, $firstOnly = true)
  106. {
  107. $nodes = null === $node ? $this->_nodes : [$node];
  108. $results = [];
  109. foreach ($nodes as $node) {
  110. $result = $this->dfs($node, $node);
  111. if ($result) {
  112. if ($firstOnly) {
  113. return $result;
  114. } else {
  115. $results[] = $result;
  116. }
  117. }
  118. }
  119. return $results;
  120. }
  121. /**
  122. * Find paths to reachable nodes from root node
  123. *
  124. * Returns array of paths, key is destination node and value is path (an array) from rootNode to destination node
  125. * Eg. dest => [root, A, dest] means root -> A -> dest
  126. *
  127. * @param string|int $rootNode
  128. * @param int $mode
  129. * @return array
  130. */
  131. public function findPathsToReachableNodes($rootNode, $mode = self::DIRECTIONAL)
  132. {
  133. $graph = $this->getRelations($mode);
  134. $paths = [];
  135. $queue = [$rootNode];
  136. $visited = [$rootNode => $rootNode];
  137. $paths[$rootNode] = [$rootNode];
  138. while (!empty($queue)) {
  139. $node = array_shift($queue);
  140. if (!empty($graph[$node])) {
  141. foreach ($graph[$node] as $child) {
  142. if (!isset($visited[$child])) {
  143. $paths[$child] = $paths[$node];
  144. $paths[$child][] = $child;
  145. $visited[$child] = $child;
  146. $queue[] = $child;
  147. }
  148. }
  149. }
  150. }
  151. return $paths;
  152. }
  153. /**
  154. * "Depth-first search" of a path between nodes
  155. *
  156. * Returns path as array of nodes or empty array if path does not exist.
  157. * Only first found path is returned. It will be not necessary the shortest or optimal in any way.
  158. *
  159. * @param string|int $fromNode
  160. * @param string|int $toNode
  161. * @param int $mode
  162. * @return array
  163. */
  164. public function dfs($fromNode, $toNode, $mode = self::DIRECTIONAL)
  165. {
  166. $this->_assertNode($fromNode, true);
  167. $this->_assertNode($toNode, true);
  168. return $this->_dfs($fromNode, $toNode, $this->getRelations($mode));
  169. }
  170. /**
  171. * Recursive sub-routine of dfs()
  172. *
  173. * @param string|int $fromNode
  174. * @param string|int $toNode
  175. * @param array $graph
  176. * @param array &$visited
  177. * @param array $stack
  178. * @return array
  179. * @link http://en.wikipedia.org/wiki/Depth-first_search
  180. */
  181. protected function _dfs($fromNode, $toNode, $graph, &$visited = [], $stack = [])
  182. {
  183. $stack[] = $fromNode;
  184. $visited[$fromNode] = $fromNode;
  185. if (isset($graph[$fromNode][$toNode])) {
  186. $stack[] = $toNode;
  187. return $stack;
  188. }
  189. if (isset($graph[$fromNode])) {
  190. foreach ($graph[$fromNode] as $node) {
  191. if (!isset($visited[$node])) {
  192. $result = $this->_dfs($node, $toNode, $graph, $visited, $stack);
  193. if ($result) {
  194. return $result;
  195. }
  196. }
  197. }
  198. }
  199. return [];
  200. }
  201. /**
  202. * Verify existence or non-existence of a node
  203. *
  204. * @param string|int $node
  205. * @param bool $mustExist
  206. * @return void
  207. * @throws \InvalidArgumentException according to assertion rules
  208. */
  209. protected function _assertNode($node, $mustExist)
  210. {
  211. if (isset($this->_nodes[$node])) {
  212. if (!$mustExist) {
  213. throw new \InvalidArgumentException("Graph node '{$node}' already exists'.");
  214. }
  215. } else {
  216. if ($mustExist) {
  217. throw new \InvalidArgumentException("Graph node '{$node}' does not exist.");
  218. }
  219. }
  220. }
  221. }