Skip to content
This repository was archived by the owner on May 24, 2023. It is now read-only.

Commit 9a970e3

Browse files
committed
Add shortestPath(s) functions
Fixes #27
1 parent 85bc555 commit 9a970e3

File tree

5 files changed

+233
-0
lines changed

5 files changed

+233
-0
lines changed

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# 0.1.3
22

3+
- Added `shortestPath` and `shortestPaths` functions.
4+
35
- Use `HashMap` and `HashSet` from `dart:collection` for
46
`stronglyConnectedComponents`. Improves runtime performance.
57

Diff for: benchmark/shortest_path_benchmark.dart

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:collection';
6+
import 'dart:math' show Random;
7+
8+
import 'package:graphs/graphs.dart';
9+
10+
void main() {
11+
final _rnd = Random(1);
12+
final size = 1000;
13+
final graph = HashMap<int, List<int>>();
14+
15+
for (var i = 0; i < size * 5; i++) {
16+
final toList = graph.putIfAbsent(_rnd.nextInt(size), () => List<int>());
17+
18+
final toValue = _rnd.nextInt(size);
19+
if (!toList.contains(toValue)) {
20+
toList.add(toValue);
21+
}
22+
}
23+
24+
var counts = <int>[];
25+
26+
final testOutput =
27+
shortestPath(0, size - 1, (e) => graph[e] ?? []).toString();
28+
print(testOutput);
29+
assert(testOutput == '[258, 252, 819, 999]');
30+
31+
for (var i = 0; i < 50; i++) {
32+
var count = 0;
33+
final watch = Stopwatch()..start();
34+
while (watch.elapsed < const Duration(milliseconds: 100)) {
35+
count++;
36+
final length = shortestPath(0, size - 1, (e) => graph[e] ?? []).length;
37+
assert(length == 4, '$length');
38+
}
39+
print(count);
40+
counts.add(count);
41+
}
42+
43+
print('max iterations in 1s: ${(counts..sort()).last}');
44+
}

Diff for: lib/graphs.dart

+1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
export 'src/crawl_async.dart' show crawlAsync;
6+
export 'src/shortest_path.dart' show shortestPath, shortestPaths;
67
export 'src/strongly_connected_components.dart'
78
show stronglyConnectedComponents;

Diff for: lib/src/shortest_path.dart

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:collection';
6+
7+
/// Returns the shortest path from [start] to [target] given the directed
8+
/// edges of a graph provided by [edges].
9+
///
10+
/// If [start] `==` [target], an empty [List] is returned and [edges] is never
11+
/// called.
12+
///
13+
/// [start], [target] and all values returned by [edges] must not be `null`.
14+
/// If asserts are enabled, an [AssertionError] is raised if these conditions
15+
/// are not met. If asserts are not enabled, violations result in undefined
16+
/// behavior.
17+
///
18+
/// This function assumes [T] implements correct and efficient `==` and
19+
/// `hashCode`.
20+
List<T> shortestPath<T>(T start, T target, Iterable<T> Function(T) edges) =>
21+
_shortestPaths(start, edges, target)[target];
22+
23+
/// Returns a [Map] of the shortest paths from [start] to all of the nodes in
24+
/// the directed graph defined by [edges].
25+
///
26+
/// All return values will contain the key [start] with an empty [List] value.
27+
///
28+
/// [start] and all values returned by [edges] must not be `null`.
29+
/// If asserts are enabled, an [AssertionError] is raised if these conditions
30+
/// are not met. If asserts are not enabled, violations result in undefined
31+
/// behavior.
32+
///
33+
/// This function assumes [T] implements correct and efficient `==` and
34+
/// `hashCode`.
35+
Map<T, List<T>> shortestPaths<T>(T start, Iterable<T> Function(T) edges) =>
36+
_shortestPaths(start, edges);
37+
38+
Map<T, List<T>> _shortestPaths<T>(T start, Iterable<T> Function(T) edges,
39+
[T target]) {
40+
assert(start != null, '`start` cannot be null');
41+
assert(edges != null, '`edges` cannot be null');
42+
43+
final distances = HashMap<T, List<T>>();
44+
distances[start] = [];
45+
46+
if (start == target) {
47+
return distances;
48+
}
49+
50+
final toVisit = ListQueue<T>()..add(start);
51+
52+
List<T> bestOption;
53+
54+
while (toVisit.isNotEmpty) {
55+
final current = toVisit.removeFirst();
56+
final distanceToCurrent = distances[current];
57+
58+
if (bestOption != null && distanceToCurrent.length >= bestOption.length) {
59+
// Skip any existing `toVisit` items that have no chance of being
60+
// better than bestOption (if it exists)
61+
continue;
62+
}
63+
64+
for (var edge in edges(current)) {
65+
assert(edge != null, '`edges` cannot return null values.');
66+
final existingPath = distances[edge];
67+
68+
if (existingPath == null ||
69+
existingPath.length > (distanceToCurrent.length + 1)) {
70+
final newOption = distanceToCurrent.followedBy(<T>[edge]).toList();
71+
72+
if (edge == target) {
73+
assert(bestOption == null || bestOption.length > newOption.length);
74+
bestOption = newOption;
75+
}
76+
77+
distances[edge] = newOption;
78+
if (bestOption == null || bestOption.length > newOption.length) {
79+
// Only add a node to visit if it might be a better path to the
80+
// target node
81+
toVisit.add(edge);
82+
}
83+
}
84+
}
85+
}
86+
87+
return distances;
88+
}

Diff for: test/shortest_path_test.dart

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:graphs/graphs.dart';
6+
import 'package:test/test.dart';
7+
8+
Matcher _throwsAssertionError(messageMatcher) =>
9+
throwsA(const TypeMatcher<AssertionError>()
10+
.having((ae) => ae.message, 'message', messageMatcher));
11+
12+
void main() {
13+
const graph = <String, List<String>>{
14+
'a': ['b', 'e'],
15+
'b': ['c'],
16+
'c': ['d', 'e'],
17+
'd': ['a'],
18+
'e': ['h'],
19+
'f': ['g'],
20+
};
21+
22+
test('null `start` throws AssertionError', () {
23+
expect(() => shortestPath(null, 'a', (input) => graph[input] ?? []),
24+
_throwsAssertionError('`start` cannot be null'));
25+
expect(() => shortestPaths(null, (input) => graph[input] ?? []),
26+
_throwsAssertionError('`start` cannot be null'));
27+
});
28+
29+
test('null `edges` throws AssertionError', () {
30+
expect(() => shortestPath('a', 'a', null),
31+
_throwsAssertionError('`edges` cannot be null'));
32+
expect(() => shortestPaths('a', null),
33+
_throwsAssertionError('`edges` cannot be null'));
34+
});
35+
36+
test('null return value from `edges` throws', () {
37+
expect(shortestPath('a', 'a', (input) => null), [],
38+
reason: 'self target short-circuits');
39+
expect(shortestPath('a', 'a', (input) => [null]), [],
40+
reason: 'self target short-circuits');
41+
42+
expect(
43+
() => shortestPath('a', 'b', (input) => null), throwsNoSuchMethodError);
44+
45+
expect(() => shortestPaths('a', (input) => null), throwsNoSuchMethodError);
46+
47+
expect(() => shortestPath('a', 'b', (input) => [null]),
48+
_throwsAssertionError('`edges` cannot return null values.'));
49+
expect(() => shortestPaths('a', (input) => [null]),
50+
_throwsAssertionError('`edges` cannot return null values.'));
51+
});
52+
53+
void _singlePathTest(String from, String to, List<String> expected) {
54+
test('$from -> $to should be $expected', () {
55+
expect(shortestPath(from, to, (input) => graph[input] ?? []), expected);
56+
});
57+
}
58+
59+
void _pathsTest(
60+
String from, Map<String, List<String>> expected, List<String> nullPaths) {
61+
test('paths from $from', () {
62+
final result = shortestPaths(from, (input) => graph[input] ?? []);
63+
expect(result, expected);
64+
});
65+
66+
for (var entry in expected.entries) {
67+
_singlePathTest(from, entry.key, entry.value);
68+
}
69+
70+
for (var entry in nullPaths) {
71+
_singlePathTest(from, entry, null);
72+
}
73+
}
74+
75+
_pathsTest('a', {
76+
'e': ['e'],
77+
'c': ['b', 'c'],
78+
'h': ['e', 'h'],
79+
'a': [],
80+
'b': ['b'],
81+
'd': ['b', 'c', 'd'],
82+
}, [
83+
'f',
84+
'g',
85+
]);
86+
87+
_pathsTest('f', {
88+
'g': ['g'],
89+
'f': [],
90+
}, [
91+
'a',
92+
]);
93+
_pathsTest('g', {'g': []}, ['a', 'f']);
94+
95+
_pathsTest('not_defined', {'not_defined': []}, ['a', 'f']);
96+
97+
_singlePathTest('a', null, null);
98+
}

0 commit comments

Comments
 (0)