Skip to content

Commit

Permalink
Implement Stoer–Wagner min-cut algorithm.
Browse files Browse the repository at this point in the history
  • Loading branch information
renggli committed Dec 26, 2023
1 parent 15b357a commit b7c951f
Show file tree
Hide file tree
Showing 6 changed files with 338 additions and 3 deletions.
2 changes: 2 additions & 0 deletions lib/graph.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ export 'src/graph/algorithms.dart' show AlgorithmsGraphExtension;
export 'src/graph/algorithms/a_star_search.dart' show AStarSearchIterable;
export 'src/graph/algorithms/dijkstra_search.dart' show DijkstraSearchIterable;
export 'src/graph/algorithms/dinic_max_flow.dart' show DinicMaxFlow;
export 'src/graph/algorithms/stoer_wagner_min_cut.dart' show StoerWagnerMinCut;
export 'src/graph/edge.dart' show Edge;
export 'src/graph/errors.dart' show GraphError;
export 'src/graph/factory.dart' show GraphFactory;
export 'src/graph/factory/atlas.dart' show AtlasGraphFactoryExtension;
export 'src/graph/factory/collection.dart' show CollectionGraphFactoryExtension;
Expand Down
17 changes: 17 additions & 0 deletions lib/src/graph/algorithms.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,23 @@ extension AlgorithmsGraphExtension<V, E> on Graph<V, E> {
vertexStrategy: vertexStrategy ?? this.vertexStrategy,
);

/// Returns an object that computes the min-cut using the Stoer-Wagner
/// algorithm.
///
/// - [edgeWeight] is a function function that returns the positive weight
/// between two edges. If no function is provided, the numeric edge value
/// or a constant weight of _1_ is used.
///
StoerWagnerMinCut<V, E> minCut({
num Function(V source, V target)? edgeWeight,
StorageStrategy<V>? vertexStrategy,
}) =>
StoerWagnerMinCut<V, E>(
graph: this,
edgeWeight: edgeWeight ?? _getDefaultEdgeValueOr(1),
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
11 changes: 8 additions & 3 deletions lib/src/graph/algorithms/dinic_max_flow.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import 'dart:collection';
import 'dart:math' as math;

import '../../../functional.dart';
import '../errors.dart';
import '../strategy.dart';
import '../traverse/depth_first.dart';

/// Dinic max flow algorithm in O(V^2*E).
/// Dinic max flow algorithm in _O(V^2*E)_.
///
/// See https://en.wikipedia.org/wiki/Dinic%27s_algorithm.
class DinicMaxFlow<V> {
Expand Down Expand Up @@ -59,8 +60,12 @@ class DinicMaxFlow<V> {
/// Computes the maximum flow between [source] and [target].
num call(V source, V target) {
final mappedSource = _mapping[source], mappedTarget = _mapping[target];
if (mappedSource == null) throw ArgumentError.value(source, 'source');
if (mappedTarget == null) throw ArgumentError.value(target, 'target');
if (mappedSource == null) {
throw GraphError(source, 'source', 'Unknown vertex');
}
if (mappedTarget == null) {
throw GraphError(target, 'target', 'Unknown vertex');
}
return _maxFlow(mappedSource, mappedTarget);
}

Expand Down
165 changes: 165 additions & 0 deletions lib/src/graph/algorithms/stoer_wagner_min_cut.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// https://github.com/thomasjungblut/tjungblut-graph/blob/master/src/de/jungblut/graph/partition/StoerWagnerMinCut.java

import 'package:collection/collection.dart';

import '../../../collection.dart';
import '../../../graph.dart';
import '../../functional/types.dart';

/// Stoer–Wagner minimum cut algorithm in _O(V*E + V*log(V))_.
///
/// See https://en.wikipedia.org/wiki/Stoer%E2%80%93Wagner_algorithm.
class StoerWagnerMinCut<V, E> {
StoerWagnerMinCut({
required Graph<V, E> graph,
num Function(V source, V target)? edgeWeight,
StorageStrategy<V>? vertexStrategy,
}) : this._(
graph: graph,
edgeWeight: edgeWeight ?? constantFunction2(1),
vertexStrategy: vertexStrategy ?? StorageStrategy.defaultStrategy(),
);

StoerWagnerMinCut._({
required this.graph,
required this.edgeWeight,
required this.vertexStrategy,
}) {
GraphError.checkUndirected(graph);
GraphError.checkVertexCount(graph, 2);

// Initialize the vertices of the working graph.
final vertexMap = graph.vertexStrategy.createMap<Set<V>>();
for (final vertex in graph.vertices) {
final list = graph.vertexStrategy.createSet();
_workingGraph.addVertex(list);
vertexMap[vertex] = list;
list.add(vertex);
}

// Initialize the edges of the working graph.
for (final edge in graph.edges.unique()) {
final weight = edgeWeight(edge.source, edge.target);
if (weight < 0) {
throw GraphError(
weight, 'edgeWeight', 'Expected positive edge weight for $edge');
}
_workingGraph.addEdge(vertexMap[edge.source]!, vertexMap[edge.target]!,
value: weight);
}

// Perform the minimum cut of Stoer–Wagner.
final vertex = _workingGraph.vertices.first;
while (_workingGraph.vertices.length > 1) {
_minimumCutPhase(vertex);
}
}

// Internal state.
final Graph<Set<V>, num> _workingGraph =
Graph<Set<V>, num>.undirected(vertexStrategy: StorageStrategy.identity());
Set<V> _bestPartition = const {};
num _bestWeight = double.infinity;

/// The underlying graph on which this cut was computed.
final Graph<V, E> graph;

/// The edge weight to be used.
final num Function(V source, V target) edgeWeight;

/// The vertex strategy to store vertices of type V.
final StorageStrategy<V> vertexStrategy;

/// Returns a view of the graph onto the first side of the cut.
Graph<V, E> get first =>
graph.where(vertexPredicate: _bestPartition.contains);

/// Returns a view of the graph onto the second side of the cut.
Graph<V, E> get second {
final vertices = vertexStrategy.createSet();
vertices.addAll(graph.vertices);
vertices.removeAll(_bestPartition);
return graph.where(vertexPredicate: vertices.contains);
}

/// Returns an iterable over the edges that are cut.
Iterable<Edge<V, E>> get edges => graph.edges.where((edge) =>
_bestPartition.contains(edge.source) !=
_bestPartition.contains(edge.target));

/// Returns the weight of the cut vertices.
num get weight => _bestWeight;

void _minimumCutPhase(Set<V> seed) {
final queue = PriorityQueue<_VertexWeight<V>>();
final mapping = <Set<V>, _VertexWeight<V>>{};
var current = seed, previous = vertexStrategy.createSet();
for (final vertex in _workingGraph.vertices) {
if (vertex == seed) continue;
final edge = _workingGraph.getEdge(vertex, seed);
final data = _VertexWeight<V>(vertex, edge?.value ?? 0, edge != null);
queue.add(data);
mapping[vertex] = data;
}
while (queue.isNotEmpty) {
final source = queue.removeFirst().vertex;
mapping.remove(source);
previous = current;
current = source;
for (final edge in _workingGraph.outgoingEdgesOf(source)) {
final target = edge.target;
final data = mapping[target];
if (data != null) {
queue.remove(data);
data.active = true;
data.weight += edge.value;
queue.add(data);
}
}
}
final weight =
_workingGraph.incomingEdgesOf(current).map((edge) => edge.value).sum;
if (weight < _bestWeight) {
_bestPartition = current;
_bestWeight = weight;
}
_mergeVertices(previous, current);
}

void _mergeVertices(Set<V> source, Set<V> target) {
final merged = vertexStrategy.createSet()
..addAll(source)
..addAll(target);
_workingGraph.addVertex(merged);
for (final vertex in _workingGraph.vertices) {
if (source != vertex && target != vertex) {
num mergedWeight = 0;
final sourceEdge = _workingGraph.getEdge(vertex, source);
if (sourceEdge != null) mergedWeight += sourceEdge.value;
final targetEdge = _workingGraph.getEdge(vertex, target);
if (targetEdge != null) mergedWeight += targetEdge.value;
if (targetEdge != null || sourceEdge != null) {
_workingGraph.addEdge(merged, vertex, value: mergedWeight);
}
}
}
_workingGraph.removeVertex(source);
_workingGraph.removeVertex(target);
}
}

class _VertexWeight<V> implements Comparable<_VertexWeight<V>> {
_VertexWeight(this.vertex, this.weight, this.active);

final Set<V> vertex;
num weight;
bool active;

@override
int compareTo(_VertexWeight<V> other) {
if (active && other.active) return -weight.compareTo(other.weight);
if (active && !other.active) return -1;
if (!active && other.active) return 1;
return 0;
}
}
22 changes: 22 additions & 0 deletions lib/src/graph/errors.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'graph.dart';

/// Error thrown when encountering unexpected graph types.
class GraphError extends ArgumentError {
/// Asserts that [graph] is undirected.
static void checkUndirected<V, E>(Graph<V, E> graph, [String? name]) {
if (graph.isDirected) {
throw GraphError(graph, name, 'Graph must be undirected');
}
}

/// Asserts that [graph] has at least at least [count] vertices.
static void checkVertexCount<V, E>(Graph<V, E> graph, int count,
[String? name]) {
if (graph.vertices.length < count) {
throw GraphError(graph, name, 'Graph must have at least $count vertices');
}
}

/// Constructs a generic [GraphError].
GraphError(super.value, [super.name, super.message]) : super.value();
}
124 changes: 124 additions & 0 deletions test/graph_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2458,6 +2458,130 @@ void main() {
expect(flow('t', 's'), 0);
});
});
group('min cut', () {
test('example 1', () {
final graph = Graph<int, int>.undirected();
graph.addEdge(1, 2, value: 2);
graph.addEdge(1, 5, value: 3);
graph.addEdge(2, 1, value: 2);
graph.addEdge(2, 3, value: 3);
graph.addEdge(2, 5, value: 2);
graph.addEdge(2, 6, value: 2);
graph.addEdge(3, 2, value: 3);
graph.addEdge(3, 4, value: 4);
graph.addEdge(3, 7, value: 2);
graph.addEdge(4, 3, value: 4);
graph.addEdge(4, 7, value: 2);
graph.addEdge(4, 8, value: 2);
graph.addEdge(5, 1, value: 3);
graph.addEdge(5, 6, value: 3);
graph.addEdge(5, 2, value: 2);
graph.addEdge(6, 2, value: 2);
graph.addEdge(6, 5, value: 3);
graph.addEdge(6, 7, value: 1);
graph.addEdge(7, 6, value: 1);
graph.addEdge(7, 3, value: 2);
graph.addEdge(7, 4, value: 2);
graph.addEdge(7, 8, value: 3);
graph.addEdge(8, 4, value: 2);
graph.addEdge(8, 7, value: 3);
final minCut = graph.minCut();
expect(minCut.first.vertices, [3, 4, 7, 8]);
expect(minCut.second.vertices, [1, 2, 5, 6]);
expect(
minCut.edges,
unorderedEquals([
isEdge(2, 3, value: 3),
isEdge(3, 2, value: 3),
isEdge(6, 7, value: 1),
isEdge(7, 6, value: 1),
]));
expect(minCut.weight, 4);
});
test('example 2', () {
final graph = Graph<int, void>.undirected();
graph.addEdge(0, 3);
graph.addEdge(3, 2);
graph.addEdge(2, 1);
graph.addEdge(1, 0);
graph.addEdge(0, 2);
graph.addEdge(2, 4);
graph.addEdge(4, 1);
final minCut = graph.minCut();
expect(minCut.first.vertices, unorderedEquals([3]));
expect(minCut.second.vertices, unorderedEquals([0, 1, 2, 4]));
expect(
minCut.edges,
unorderedEquals([
isEdge(0, 3),
isEdge(3, 0),
isEdge(2, 3),
isEdge(3, 2),
]));
expect(minCut.weight, 2);
});
test('example 3', () {
final graph = Graph<int, int>.undirected();
graph.addEdge(0, 1, value: 2);
graph.addEdge(0, 4, value: 3);
graph.addEdge(1, 2, value: 3);
graph.addEdge(1, 4, value: 2);
graph.addEdge(1, 5, value: 2);
graph.addEdge(2, 3, value: 4);
graph.addEdge(2, 6, value: 2);
graph.addEdge(3, 6, value: 2);
graph.addEdge(3, 7, value: 2);
graph.addEdge(4, 5, value: 3);
graph.addEdge(5, 6, value: 1);
graph.addEdge(6, 7, value: 3);
final minCut = graph.minCut();
expect(minCut.first.vertices, unorderedEquals([2, 3, 6, 7]));
expect(minCut.second.vertices, unorderedEquals([0, 1, 4, 5]));
expect(
minCut.edges,
unorderedEquals([
isEdge(1, 2, value: 3),
isEdge(2, 1, value: 3),
isEdge(5, 6, value: 1),
isEdge(6, 5, value: 1),
]));
expect(minCut.weight, 4);
});
test('example 4', () {
final graph = Graph<String, int>.undirected();
graph.addEdge('x', 'a', value: 3);
graph.addEdge('x', 'b', value: 1);
graph.addEdge('a', 'c', value: 3);
graph.addEdge('b', 'c', value: 5);
graph.addEdge('b', 'd', value: 4);
graph.addEdge('d', 'e', value: 2);
graph.addEdge('c', 'y', value: 2);
graph.addEdge('e', 'y', value: 3);
final minCut = graph.minCut();
expect(minCut.first.vertices, unorderedEquals(['e', 'y']));
expect(
minCut.second.vertices, unorderedEquals(['x', 'a', 'b', 'c', 'd']));
expect(
minCut.edges,
unorderedEquals([
isEdge('c', 'y', value: 2),
isEdge('y', 'c', value: 2),
isEdge('d', 'e', value: 2),
isEdge('e', 'd', value: 2),
]));
expect(minCut.weight, 4);
});
test('empty graph errors', () {
final graph = Graph<String, void>.undirected();
expect(graph.minCut, throwsArgumentError);
});
test('directed graph errors', () {
final graph = Graph<int, void>.directed()
..addEdge(0, 1)
..addEdge(1, 2);
expect(graph.minCut, throwsArgumentError);
});
});
});
group('traverse', () {
group('breadth-first', () {
Expand Down

0 comments on commit b7c951f

Please sign in to comment.