| // Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:collection'; |
| |
| import 'cycle_exception.dart'; |
| import 'strongly_connected_components.dart'; |
| import 'topological_sort.dart'; |
| |
| /// Returns a transitive closure of a directed graph provided by [nodes] and |
| /// [edges]. |
| /// |
| /// The result is a map from [nodes] to the sets of nodes that are transitively |
| /// reachable through [edges]. No particular ordering is guaranteed. |
| /// |
| /// If [equals] is provided, it is used to compare nodes in the graph. If |
| /// [equals] is omitted, the node's own [Object.==] is used instead. |
| /// |
| /// Similarly, if [hashCode] is provided, it is used to produce a hash value |
| /// for nodes to efficiently calculate the return value. If it is omitted, the |
| /// key's own [Object.hashCode] is used. |
| /// |
| /// If you supply one of [equals] or [hashCode], you should generally also to |
| /// supply the other. |
| /// |
| /// Note: this requires that [nodes] and each iterable returned by [edges] |
| /// contain no duplicate entries. |
| /// |
| /// By default, this can handle either cyclic or acyclic graphs. If [acyclic] is |
| /// true, this will run more efficiently but throw a [CycleException] if the |
| /// graph is cyclical. |
| Map<T, Set<T>> transitiveClosure<T extends Object>( |
| Iterable<T> nodes, |
| Iterable<T> Function(T) edges, { |
| bool Function(T, T)? equals, |
| int Function(T)? hashCode, |
| bool acyclic = false, |
| }) { |
| if (!acyclic) { |
| return _cyclicTransitiveClosure( |
| nodes, |
| edges, |
| equals: equals, |
| hashCode: hashCode, |
| ); |
| } |
| |
| final topologicalOrder = |
| topologicalSort(nodes, edges, equals: equals, hashCode: hashCode); |
| final result = LinkedHashMap<T, Set<T>>(equals: equals, hashCode: hashCode); |
| for (final node in topologicalOrder.reversed) { |
| final closure = LinkedHashSet<T>(equals: equals, hashCode: hashCode); |
| for (var child in edges(node)) { |
| closure.add(child); |
| closure.addAll(result[child]!); |
| } |
| |
| result[node] = closure; |
| } |
| |
| return result; |
| } |
| |
| /// Returns the transitive closure of a cyclic graph using [Purdom's algorithm]. |
| /// |
| /// [Purdom's algorithm]: https://algowiki-project.org/en/Purdom%27s_algorithm |
| /// |
| /// This first computes the strongly connected components of the graph and finds |
| /// the transitive closure of those before flattening it out into the transitive |
| /// closure of the entire graph. |
| Map<T, Set<T>> _cyclicTransitiveClosure<T extends Object>( |
| Iterable<T> nodes, |
| Iterable<T> Function(T) edges, { |
| bool Function(T, T)? equals, |
| int Function(T)? hashCode, |
| }) { |
| final components = stronglyConnectedComponents<T>( |
| nodes, |
| edges, |
| equals: equals, |
| hashCode: hashCode, |
| ); |
| final nodesToComponents = |
| HashMap<T, List<T>>(equals: equals, hashCode: hashCode); |
| for (final component in components) { |
| for (final node in component) { |
| nodesToComponents[node] = component; |
| } |
| } |
| |
| // Because [stronglyConnectedComponents] returns the components in reverse |
| // topological order, we can avoid an additional topological sort here. |
| // Instead, we directly traverse the component list with the knowledge that |
| // once we reach a component, everything reachable from it has already been |
| // registered in [result]. |
| final result = LinkedHashMap<T, Set<T>>(equals: equals, hashCode: hashCode); |
| for (final component in components) { |
| final closure = LinkedHashSet<T>(equals: equals, hashCode: hashCode); |
| if (_componentIncludesCycle(component, edges, equals)) { |
| closure.addAll(component); |
| } |
| |
| // De-duplicate downstream components to avoid adding the same transitive |
| // children over and over. |
| final downstreamComponents = { |
| for (final node in component) |
| for (final child in edges(node)) nodesToComponents[child]!, |
| }; |
| for (final childComponent in downstreamComponents) { |
| if (childComponent == component) continue; |
| |
| // This if check is just for efficiency. If [childComponent] has multiple |
| // nodes, `result[childComponent.first]` will contain all the nodes in |
| // `childComponent` anyway since it's cyclical. |
| if (childComponent.length == 1) closure.addAll(childComponent); |
| closure.addAll(result[childComponent.first]!); |
| } |
| |
| for (final node in component) { |
| result[node] = closure; |
| } |
| } |
| return result; |
| } |
| |
| /// Returns whether the strongly-connected component [component] of a graph |
| /// defined by [edges] includes a cycle. |
| bool _componentIncludesCycle<T>( |
| List<T> component, |
| Iterable<T> Function(T) edges, |
| bool Function(T, T)? equals, |
| ) { |
| // A strongly-connected component with more than one node always contains a |
| // cycle, by definition. |
| if (component.length > 1) return true; |
| |
| // A component with only a single node only contains a cycle if that node has |
| // an edge to itself. |
| final node = component.single; |
| return edges(node) |
| .any((edge) => equals == null ? edge == node : equals(edge, node)); |
| } |