shortest_path 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +6 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +30 -0
- data/Rakefile +10 -0
- data/lib/shortest_path/finder.rb +118 -0
- data/lib/shortest_path/map.rb +70 -0
- data/lib/shortest_path/version.rb +3 -0
- data/lib/shortest_path.rb +10 -0
- data/shortest_path.gemspec +27 -0
- data/spec/shortest_path/finder_spec.rb +124 -0
- data/spec/shortest_path/map_spec.rb +23 -0
- data/spec/spec_helper.rb +12 -0
- metadata +135 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010-2012 Dryade SAS
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# Shortest Path
|
2
|
+
|
3
|
+
A* ruby implementation to find shortest path and map in a graph.
|
4
|
+
|
5
|
+
## Requirements
|
6
|
+
|
7
|
+
This code has been run and tested on Ruby 1.8
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
This package is available in RubyGems and can be installed with:
|
12
|
+
|
13
|
+
gem install shortest_path
|
14
|
+
|
15
|
+
## More Information
|
16
|
+
|
17
|
+
More information can be found on the [project website on GitHub](http://github.com/dryade/shortest_path).
|
18
|
+
There is extensive usage documentation available [on the wiki](https://github.com/dryade/shortest_path/wiki).
|
19
|
+
|
20
|
+
## Example Usage
|
21
|
+
|
22
|
+
...
|
23
|
+
|
24
|
+
## License
|
25
|
+
|
26
|
+
This project is licensed under the MIT license, a copy of which can be found in the LICENSE file.
|
27
|
+
|
28
|
+
## Support
|
29
|
+
|
30
|
+
Users looking for support should file an issue on the GitHub issue tracking page (https://github.com/dryade/shortest_path/issues), or file a pull request (https://github.com/dryade/shortest_path/pulls) if you have a fix available.
|
data/Rakefile
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'pqueue'
|
2
|
+
|
3
|
+
module ShortestPath
|
4
|
+
class Finder
|
5
|
+
|
6
|
+
attr_reader :source, :destination
|
7
|
+
|
8
|
+
def initialize(source, destination)
|
9
|
+
@source, @destination = source, destination
|
10
|
+
@visited = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
# Should return a map with accessible nodes and associated weight
|
14
|
+
# Example : { :a => 2, :b => 3 }
|
15
|
+
attr_accessor :ways_finder
|
16
|
+
|
17
|
+
def ways(node)
|
18
|
+
ways_finder.call node
|
19
|
+
end
|
20
|
+
|
21
|
+
def shortest_distances
|
22
|
+
@shortest_distances ||= {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def previous
|
26
|
+
@previous ||= {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def search_heuristic(node)
|
30
|
+
shortest_distances[node]
|
31
|
+
end
|
32
|
+
|
33
|
+
def follow_way?(node, destination, weight)
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_accessor :timeout
|
38
|
+
attr_reader :begin_at, :end_at
|
39
|
+
|
40
|
+
def timeout?
|
41
|
+
timeout and (duration > timeout)
|
42
|
+
end
|
43
|
+
|
44
|
+
def duration
|
45
|
+
return nil unless begin_at
|
46
|
+
(end_at or Time.now) - begin_at
|
47
|
+
end
|
48
|
+
|
49
|
+
def visited?(node)
|
50
|
+
@visited[node]
|
51
|
+
end
|
52
|
+
|
53
|
+
def visit(node)
|
54
|
+
@visited[node] = true
|
55
|
+
end
|
56
|
+
|
57
|
+
def found?(node)
|
58
|
+
node == destination
|
59
|
+
end
|
60
|
+
|
61
|
+
def path_without_cache
|
62
|
+
@begin_at = Time.now
|
63
|
+
|
64
|
+
visited = {}
|
65
|
+
pq = PQueue.new do |x,y|
|
66
|
+
search_heuristic(x) < search_heuristic(y)
|
67
|
+
end
|
68
|
+
|
69
|
+
pq.push(source)
|
70
|
+
visit source
|
71
|
+
shortest_distances[source] = 0
|
72
|
+
|
73
|
+
not_found = !found?(source)
|
74
|
+
|
75
|
+
while pq.size != 0 && not_found
|
76
|
+
raise TimeoutError if timeout?
|
77
|
+
|
78
|
+
v = pq.pop
|
79
|
+
not_found = !found?(v)
|
80
|
+
visit v
|
81
|
+
|
82
|
+
weights = ways(v)
|
83
|
+
if weights
|
84
|
+
weights.keys.each do |w|
|
85
|
+
if !visited?(w) and
|
86
|
+
weights[w] and
|
87
|
+
( shortest_distances[w].nil? || shortest_distances[w] > shortest_distances[v] + weights[w]) and
|
88
|
+
follow_way?(v, w, weights[w])
|
89
|
+
shortest_distances[w] = shortest_distances[v] + weights[w]
|
90
|
+
previous[w] = v
|
91
|
+
pq.push(w)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
@end_at = Time.now
|
98
|
+
not_found ? [] : sorted_array
|
99
|
+
end
|
100
|
+
|
101
|
+
def path_with_cache
|
102
|
+
@path ||= path_without_cache
|
103
|
+
end
|
104
|
+
alias_method :path, :path_with_cache
|
105
|
+
|
106
|
+
def sorted_array
|
107
|
+
[].tap do |sorted_array|
|
108
|
+
previous_id = destination
|
109
|
+
previous.size.times do |t|
|
110
|
+
sorted_array.unshift(previous_id)
|
111
|
+
break if previous_id == source
|
112
|
+
previous_id = previous[previous_id]
|
113
|
+
raise "Unknown #{previous_id.inspect}" unless previous_id
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module ShortestPath
|
2
|
+
class Map
|
3
|
+
|
4
|
+
attr_reader :source
|
5
|
+
attr_accessor :max_distance
|
6
|
+
|
7
|
+
def initialize(source)
|
8
|
+
@source = source
|
9
|
+
end
|
10
|
+
|
11
|
+
# Should return a map with accessible nodes and associated weight
|
12
|
+
# Example : { :a => 2, :b => 3 }
|
13
|
+
attr_accessor :ways_finder
|
14
|
+
|
15
|
+
def ways(node)
|
16
|
+
ways_finder.call node
|
17
|
+
end
|
18
|
+
|
19
|
+
def shortest_distances
|
20
|
+
@shortest_distances ||= {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def previous
|
24
|
+
@previous ||= {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def search_heuristic(node)
|
28
|
+
shortest_distances[node]
|
29
|
+
end
|
30
|
+
|
31
|
+
def map
|
32
|
+
@shortest_distances = {}
|
33
|
+
@previous = {}
|
34
|
+
|
35
|
+
visited = {}
|
36
|
+
pq = PQueue.new { |x,y| search_heuristic(x) < search_heuristic(y) }
|
37
|
+
|
38
|
+
pq.push(source)
|
39
|
+
visited[source] = true
|
40
|
+
shortest_distances[source] = 0
|
41
|
+
|
42
|
+
while pq.size != 0
|
43
|
+
v = pq.pop
|
44
|
+
visited[v] = true
|
45
|
+
|
46
|
+
weights = ways(v)
|
47
|
+
if weights
|
48
|
+
weights.keys.each do |w|
|
49
|
+
w_distance = shortest_distances[v] + weights[w]
|
50
|
+
|
51
|
+
if !visited[w] and
|
52
|
+
weights[w] and
|
53
|
+
( shortest_distances[w].nil? || shortest_distances[w] > w_distance) and
|
54
|
+
follow_way?(v, w, weights[w])
|
55
|
+
shortest_distances[w] = w_distance
|
56
|
+
previous[w] = v
|
57
|
+
pq.push(w)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
shortest_distances
|
64
|
+
end
|
65
|
+
|
66
|
+
def follow_way?(node, destination, weight)
|
67
|
+
max_distance.nil? or shortest_distances[node] + weight <= max_distance
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "shortest_path/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "shortest_path"
|
7
|
+
s.version = ShortestPath::VERSION
|
8
|
+
s.authors = ["Alban Peignier", "Marc Florisson"]
|
9
|
+
s.email = ["alban@dryade.net", "marc@dryade.net"]
|
10
|
+
s.homepage = "http://github.com/dryade/shortest_path"
|
11
|
+
s.summary = %q{Ruby library to find shortest path(s) in a graph}
|
12
|
+
s.description = %q{A* ruby implementation to find shortest path and map}
|
13
|
+
|
14
|
+
s.rubyforge_project = "shortest_path"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_runtime_dependency "pqueue"
|
22
|
+
|
23
|
+
s.add_development_dependency "rake"
|
24
|
+
s.add_development_dependency "rspec"
|
25
|
+
s.add_development_dependency "rcov"
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ShortestPath::Finder do
|
4
|
+
let(:graph) {
|
5
|
+
{ :a => { :e => 3, :b => 1, :c => 3},
|
6
|
+
:b => {:e => 1, :a => 1, :c => 3, :d => 5},
|
7
|
+
:c => {:a => 3, :b => 3, :d => 1, :s => 3},
|
8
|
+
:d => {:b => 5, :c => 1, :s => 1},
|
9
|
+
:e => {:a => 3, :b => 1},
|
10
|
+
:s => {:c => 3, :d => 1} }
|
11
|
+
}
|
12
|
+
|
13
|
+
def shortest_path(source, destination, given_graph = graph)
|
14
|
+
ShortestPath::Finder.new(source, destination).tap do |shortest_path|
|
15
|
+
shortest_path.ways_finder = Proc.new { |node| given_graph[node] }
|
16
|
+
end.path
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should find shortest path in an exemple" do
|
20
|
+
shortest_path(:e, :s).should == [:e, :b, :c, :d, :s]
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should return empty array when unknown start or end" do
|
24
|
+
shortest_path(:e, :unknown).should be_empty
|
25
|
+
shortest_path(:unknown, :s).should be_empty
|
26
|
+
shortest_path(:unknown, :unknown2).should be_empty
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should find trivial solution" do
|
30
|
+
shortest_path(:a, :b).should == [:a, :b]
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should return empty array when graph is not connex" do
|
34
|
+
not_connex = graph.clone
|
35
|
+
not_connex[:d].delete(:s)
|
36
|
+
not_connex[:c].delete(:s)
|
37
|
+
|
38
|
+
shortest_path(:e, :s, not_connex).should be_empty
|
39
|
+
end
|
40
|
+
|
41
|
+
subject {
|
42
|
+
ShortestPath::Finder.new(:e, :s).tap do |shortest_path|
|
43
|
+
shortest_path.ways_finder = Proc.new { |node| graph[node] }
|
44
|
+
end
|
45
|
+
}
|
46
|
+
|
47
|
+
describe "begin_at" do
|
48
|
+
|
49
|
+
let(:expected_time) { Time.now }
|
50
|
+
|
51
|
+
it "should be defined when path starts" do
|
52
|
+
Time.stub :now => expected_time
|
53
|
+
subject.path
|
54
|
+
subject.begin_at.should == expected_time
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "end_at" do
|
60
|
+
|
61
|
+
let(:expected_time) { Time.now }
|
62
|
+
|
63
|
+
it "should be defined when path ends" do
|
64
|
+
Time.stub :now => expected_time
|
65
|
+
subject.path
|
66
|
+
subject.end_at.should == expected_time
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "duration" do
|
72
|
+
|
73
|
+
it "should be nil before path is search" do
|
74
|
+
subject.duration.should be_nil
|
75
|
+
end
|
76
|
+
|
77
|
+
let(:time) { Time.now }
|
78
|
+
|
79
|
+
it "should be difference between Time.now and begin_at when path isn't ended'" do
|
80
|
+
Time.stub :now => time
|
81
|
+
subject.stub :begin_at => time - 2, :end_at => nil
|
82
|
+
subject.duration.should == 2
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should be difference between end_at and begin_at when available" do
|
86
|
+
subject.stub :begin_at => time - 2, :end_at => time
|
87
|
+
subject.duration.should == 2
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "timeout?" do
|
93
|
+
|
94
|
+
before(:each) do
|
95
|
+
subject.timeout = 2
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should be false without timeout" do
|
99
|
+
subject.timeout = nil
|
100
|
+
subject.should_not be_timeout
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should be false when duration is lower than timeout" do
|
104
|
+
subject.stub :duration => (subject.timeout - 1)
|
105
|
+
subject.should_not be_timeout
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should be true when duration is greater than timeout" do
|
109
|
+
subject.stub :duration => (subject.timeout + 1)
|
110
|
+
subject.should be_timeout
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
describe "path" do
|
116
|
+
|
117
|
+
it "should raise a Timeout::Error when timeout?" do
|
118
|
+
subject.stub :timeout? => true
|
119
|
+
lambda { subject.path }.should raise_error(ShortestPath::TimeoutError)
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ShortestPath::Map do
|
4
|
+
|
5
|
+
let(:graph) {
|
6
|
+
{ :a => { :b => 2, :d => 1 },
|
7
|
+
:b => { :c => 1, :a => 2 },
|
8
|
+
:c => { :b => 1, :d => 3 },
|
9
|
+
:d => { :a => 1, :c => 3 } }
|
10
|
+
}
|
11
|
+
|
12
|
+
def shortest_path_map(source, max_distance = nil, given_graph = graph)
|
13
|
+
ShortestPath::Map.new(source).tap do |shortest_path_map|
|
14
|
+
shortest_path_map.max_distance = nil
|
15
|
+
shortest_path_map.ways_finder = Proc.new { |node| given_graph[node] }
|
16
|
+
end.map
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should find shortest path map in an exemple" do
|
20
|
+
shortest_path_map(:a).should == { :a => 0, :b => 2, :c => 3, :d => 1 }
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'rspec'
|
5
|
+
|
6
|
+
require 'shortest_path' # and any other gems you need
|
7
|
+
|
8
|
+
Dir[File.expand_path(File.join(File.dirname(__FILE__),'support','**','*.rb'))].each {|f| require f}
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
# some (optional) config here
|
12
|
+
end
|
metadata
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: shortest_path
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Alban Peignier
|
14
|
+
- Marc Florisson
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2013-04-30 00:00:00 Z
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
type: :runtime
|
23
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
requirement: *id001
|
33
|
+
prerelease: false
|
34
|
+
name: pqueue
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
type: :development
|
37
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
hash: 3
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
version: "0"
|
46
|
+
requirement: *id002
|
47
|
+
prerelease: false
|
48
|
+
name: rake
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
type: :development
|
51
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
hash: 3
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
version: "0"
|
60
|
+
requirement: *id003
|
61
|
+
prerelease: false
|
62
|
+
name: rspec
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
type: :development
|
65
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
hash: 3
|
71
|
+
segments:
|
72
|
+
- 0
|
73
|
+
version: "0"
|
74
|
+
requirement: *id004
|
75
|
+
prerelease: false
|
76
|
+
name: rcov
|
77
|
+
description: A* ruby implementation to find shortest path and map
|
78
|
+
email:
|
79
|
+
- alban@dryade.net
|
80
|
+
- marc@dryade.net
|
81
|
+
executables: []
|
82
|
+
|
83
|
+
extensions: []
|
84
|
+
|
85
|
+
extra_rdoc_files: []
|
86
|
+
|
87
|
+
files:
|
88
|
+
- .gitignore
|
89
|
+
- Gemfile
|
90
|
+
- LICENSE
|
91
|
+
- README.md
|
92
|
+
- Rakefile
|
93
|
+
- lib/shortest_path.rb
|
94
|
+
- lib/shortest_path/finder.rb
|
95
|
+
- lib/shortest_path/map.rb
|
96
|
+
- lib/shortest_path/version.rb
|
97
|
+
- shortest_path.gemspec
|
98
|
+
- spec/shortest_path/finder_spec.rb
|
99
|
+
- spec/shortest_path/map_spec.rb
|
100
|
+
- spec/spec_helper.rb
|
101
|
+
homepage: http://github.com/dryade/shortest_path
|
102
|
+
licenses: []
|
103
|
+
|
104
|
+
post_install_message:
|
105
|
+
rdoc_options: []
|
106
|
+
|
107
|
+
require_paths:
|
108
|
+
- lib
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
110
|
+
none: false
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
hash: 3
|
115
|
+
segments:
|
116
|
+
- 0
|
117
|
+
version: "0"
|
118
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
hash: 3
|
124
|
+
segments:
|
125
|
+
- 0
|
126
|
+
version: "0"
|
127
|
+
requirements: []
|
128
|
+
|
129
|
+
rubyforge_project: shortest_path
|
130
|
+
rubygems_version: 1.8.24
|
131
|
+
signing_key:
|
132
|
+
specification_version: 3
|
133
|
+
summary: Ruby library to find shortest path(s) in a graph
|
134
|
+
test_files: []
|
135
|
+
|