// 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> transitiveClosure( Iterable nodes, Iterable 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>(equals: equals, hashCode: hashCode); for (final node in topologicalOrder.reversed) { final closure = LinkedHashSet(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> _cyclicTransitiveClosure( Iterable nodes, Iterable Function(T) edges, { bool Function(T, T)? equals, int Function(T)? hashCode, }) { final components = stronglyConnectedComponents( nodes, edges, equals: equals, hashCode: hashCode, ); final nodesToComponents = HashMap>(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>(equals: equals, hashCode: hashCode); for (final component in components) { final closure = LinkedHashSet(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( List component, Iterable 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)); }