|
| 1 | +<?php |
| 2 | +namespace Xi\Algorithm; |
| 3 | + |
| 4 | +/** |
| 5 | + * Implements topological sorting. |
| 6 | + * |
| 7 | + * Topological sorting means sorting the nodes in a directed acyclic graph into a list such |
| 8 | + * that if a node X points to a node Y then Y appears before X in the list. |
| 9 | + * This is useful for things like resolving dependencies. |
| 10 | + * |
| 11 | + * http://en.wikipedia.org/wiki/Topological_sorting |
| 12 | + */ |
| 13 | +class TopologicalSort |
| 14 | +{ |
| 15 | + /** |
| 16 | + * @param array $edges An array of scalars to arrays of scalars, representing the edges in the graph. |
| 17 | + * @return array An array of nodes in the graph. |
| 18 | + * @throws \InvalidArgumentException if the graph has a cycle or $deps is in an invalid format |
| 19 | + */ |
| 20 | + public static function apply(array $edges) |
| 21 | + { |
| 22 | + $allNodes = self::allNodes($edges); |
| 23 | + |
| 24 | + // We do a depth-first search. |
| 25 | + // A node is marked with 1 when first seen and with 2 when recursion returns from it. |
| 26 | + // If we meet a node marked 0 then we recursively add its descendants to the list before adding it to the list. |
| 27 | + // If we meet a node marked 1 then we've found a cycle, which is an error. |
| 28 | + // If we meet a node marked 2 then it's already on the list and we return. |
| 29 | + $unmarkedNodes = array_fill_keys($allNodes, null); |
| 30 | + $marks = array_fill_keys($allNodes, 0); |
| 31 | + $result = array(); |
| 32 | + |
| 33 | + $visit = function ($node) use ($edges, &$visit, &$marks, &$unmarkedNodes, &$result) { |
| 34 | + $mark = $marks[$node]; |
| 35 | + if ($mark === 0) { |
| 36 | + $marks[$node] = 1; |
| 37 | + unset($unmarkedNodes[$node]); |
| 38 | + |
| 39 | + foreach ((isset($edges[$node]) ? $edges[$node] : array()) as $next) { |
| 40 | + $visit($next); |
| 41 | + } |
| 42 | + |
| 43 | + $marks[$node] = 2; |
| 44 | + $result[] = $node; |
| 45 | + } elseif ($mark === 1) { |
| 46 | + throw new \InvalidArgumentException("The graph has a cycle involving node $node"); |
| 47 | + } |
| 48 | + }; |
| 49 | + |
| 50 | + // We try each node as a starting point since we don't know which node (if any) is the root node. |
| 51 | + // (if the graph has unconnected subgraphs then their order in the list is undefined). |
| 52 | + while (!empty($unmarkedNodes)) { |
| 53 | + $node = key($unmarkedNodes); |
| 54 | + unset($unmarkedNodes[$node]); |
| 55 | + $visit($node); |
| 56 | + } |
| 57 | + |
| 58 | + return $result; |
| 59 | + } |
| 60 | + |
| 61 | + private static function allNodes(array $deps) |
| 62 | + { |
| 63 | + $allNodes = array(); |
| 64 | + foreach ($deps as $node => $others) { |
| 65 | + if (!is_array($others)) { |
| 66 | + throw new \InvalidArgumentException('Dependencies should be given as arrays, not single elements.'); |
| 67 | + } |
| 68 | + $allNodes[] = $node; |
| 69 | + foreach ($others as $other) { |
| 70 | + $allNodes[] = $other; |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + return array_unique($allNodes); |
| 75 | + } |
| 76 | +} |
0 commit comments