Skip to content

Commit

Permalink
Add Tarjan's strongly connected components algorithm.
Browse files Browse the repository at this point in the history
  • Loading branch information
renggli committed Mar 27, 2024
1 parent 73e6993 commit c47989c
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 3 deletions.
19 changes: 17 additions & 2 deletions lib/src/graph/algorithms.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import 'package:collection/collection.dart';

import '../../functional.dart';
import '../../graph.dart';
import 'algorithms/a_star_search.dart';
import 'algorithms/dijkstra_search.dart';
import 'algorithms/dinic_max_flow.dart';
import 'algorithms/prims_min_spanning_tree.dart';
import 'algorithms/stoer_wagner_min_cut.dart';
import 'algorithms/tarjan_strongly_connected.dart';
import 'graph.dart';
import 'path.dart';
import 'strategy.dart';

extension AlgorithmsGraphExtension<V, E> on Graph<V, E> {
/// Performs a search for the shortest path between [source] and [target].
Expand Down Expand Up @@ -114,13 +121,21 @@ extension AlgorithmsGraphExtension<V, E> on Graph<V, E> {
num Function(V source, V target)? edgeWeight,
StorageStrategy<V>? vertexStrategy,
}) =>
prims(
primsMinSpanningTree<V, E>(
this,
startVertex: startVertex,
edgeWeight: edgeWeight ?? _getDefaultEdgeValueOr(1),
vertexStrategy: vertexStrategy ?? this.vertexStrategy,
);

/// Returns the strongly connected components in this graph. The
/// implementation uses the Tarjan's algorithm and runs in linear time.
TarjanStronglyConnected<V, E> stronglyConnected({
StorageStrategy<V>? vertexStrategy,
}) =>
TarjanStronglyConnected<V, E>(this,
vertexStrategy: vertexStrategy ?? this.vertexStrategy);

/// Internal helper that returns a function using the numeric edge value
/// of this graph, or otherwise a constant value for each edge.
num Function(V source, V target) _getDefaultEdgeValueOr(num value) =>
Expand Down
2 changes: 1 addition & 1 deletion lib/src/graph/algorithms/prims_min_spanning_tree.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import '../strategy.dart';
/// Prim's algorithm to find the minimum spanning tree in _O(E*log(V))_.
///
/// See https://en.wikipedia.org/wiki/Prim%27s_algorithm.
Graph<V, E> prims<V, E>(
Graph<V, E> primsMinSpanningTree<V, E>(
Graph<V, E> graph, {
required V? startVertex,
required num Function(V source, V target) edgeWeight,
Expand Down
78 changes: 78 additions & 0 deletions lib/src/graph/algorithms/tarjan_strongly_connected.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import 'dart:math';

import '../graph.dart';
import '../model/where.dart';
import '../strategy.dart';

/// Tarjan's strongly connected components.
///
/// https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm.
class TarjanStronglyConnected<V, E> {
TarjanStronglyConnected(this.graph,
{required StorageStrategy<V> vertexStrategy})
: _states = vertexStrategy.createMap<_State<V>>() {
for (final vertex in graph.vertices) {
if (!_states.containsKey(vertex)) {
_connect(vertex);
}
}
}

/// The underlying graph on which these strongly connected components are
/// computed.
final Graph<V, E> graph;

// Internal state.
final Map<V, _State<V>> _states;
final _stack = <_State<V>>[];
final _results = <Set<V>>{};

/// Returns a set of strongly connected vertices.
Set<Set<V>> get vertices => _results;

/// Returns a set of the strongly connected sub-graphs.
Set<Graph<V, E>> get graphs => _results
.map((each) => graph.where(vertexPredicate: each.contains))
.toSet();

_State<V> _connect(V vertex) {
// Set the depth to smallest unused index.
final state = _State<V>(vertex, _states.length);
_stack.add(_states[vertex] = state);
// Consider all successors of `source`.
for (final successor in graph.successorsOf(vertex)) {
final successorState = _states[successor];
if (successorState == null) {
// Successors has not yet been visited, recurse on it.
final createdState = _connect(successor);
state.lowLink = min(state.lowLink, createdState.lowLink);
} else if (!successorState.isObsolete) {
// Successor is on stack and hence in the strongly connected component.
state.lowLink = min(state.lowLink, successorState.depth);
}
}
// If vertex is a root node, pop the stack and generate an strongly
// connected component.
if (state.lowLink == state.depth) {
final result = <V>{};
while (true) {
final state = _stack.removeLast();
result.add(state.vertex);
state.isObsolete = true;
if (state.vertex == vertex) break;
}
_results.add(result);
}
// Return the state of vertex.
return state;
}
}

class _State<V> {
_State(this.vertex, this.depth) : lowLink = depth;

final V vertex;
final int depth;
int lowLink;
bool isObsolete = false;
}
102 changes: 102 additions & 0 deletions test/graph_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ import 'package:more/graph.dart';
import 'package:more/math.dart';
import 'package:test/test.dart';

@optionalTypeArgs
Matcher isGraph<E, V>({
dynamic vertices = anything,
dynamic edges = anything,
dynamic isDirected = anything,
}) =>
isA<Graph<E, V>>()
.having((graph) => graph.vertices, 'vertices', vertices)
.having((graph) => graph.edges, 'edges', edges)
.having((graph) => graph.isDirected, 'isDirected', isDirected)
.having((graph) => graph.toString(), 'toString', contains('Graph'));

@optionalTypeArgs
Matcher isEdge<E, V>(
dynamic source,
Expand Down Expand Up @@ -2774,6 +2786,96 @@ void main() {
]));
});
});
group('strongly connected', () {
test('empty graph', () {
final graph = Graph<int, void>.directed();
expect(graph.stronglyConnected().vertices, isEmpty);
expect(graph.stronglyConnected().graphs, isEmpty);
});
test('single vertex', () {
final graph = Graph<int, void>.directed();
graph.addVertex(1);
expect(graph.stronglyConnected().vertices, {
{1},
});
expect(graph.stronglyConnected().graphs, {
isGraph<int, void>(vertices: [1], edges: isEmpty, isDirected: true),
});
});
test('self-connected vertex', () {
final graph = Graph<int, void>.directed();
graph.addEdge(1, 1);
expect(graph.stronglyConnected().vertices, {
{1},
});
expect(graph.stronglyConnected().graphs, {
isGraph<int, void>(
vertices: [1], edges: [isEdge(1, 1)], isDirected: true),
});
});
test('disconnected pair', () {
final graph = Graph<int, void>.directed();
graph.addVertex(1);
graph.addVertex(2);
expect(graph.stronglyConnected().vertices, {
{1},
{2},
});
expect(graph.stronglyConnected().graphs, {
isGraph<int, void>(vertices: [1], edges: isEmpty, isDirected: true),
isGraph<int, void>(vertices: [2], edges: isEmpty, isDirected: true),
});
});
test('weakly connected pair', () {
final graph = Graph<int, void>.directed();
graph.addEdge(2, 1);
expect(graph.stronglyConnected().vertices, {
{1},
{2},
});
expect(graph.stronglyConnected().graphs, {
isGraph<int, void>(vertices: [1], edges: isEmpty, isDirected: true),
isGraph<int, void>(vertices: [2], edges: isEmpty, isDirected: true),
});
});
test('strongly connected pair', () {
final graph = Graph<int, void>.directed();
graph.addEdge(1, 2);
graph.addEdge(2, 1);
expect(graph.stronglyConnected().vertices, {
{1, 2},
});
expect(graph.stronglyConnected().graphs, {
isGraph<int, void>(
vertices: [1, 2],
edges: [isEdge(1, 2), isEdge(2, 1)],
isDirected: true),
});
});
test('wikipedia', () {
final graph = Graph<int, void>.directed();
graph.addEdge(1, 5);
graph.addEdge(2, 1);
graph.addEdge(3, 2);
graph.addEdge(3, 4);
graph.addEdge(4, 3);
graph.addEdge(5, 2);
graph.addEdge(6, 2);
graph.addEdge(6, 5);
graph.addEdge(6, 7);
graph.addEdge(7, 3);
graph.addEdge(7, 6);
graph.addEdge(8, 4);
graph.addEdge(8, 7);
graph.addEdge(8, 8);
expect(graph.stronglyConnected().vertices, {
{1, 2, 5},
{3, 4},
{6, 7},
{8}
});
});
});
});
group('traverse', () {
group('breadth-first', () {
Expand Down

0 comments on commit c47989c

Please sign in to comment.