blob: 24791d695e5d01b52168f10da81cb6cce03fe88e [file] [log] [blame]
// 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));
}