bfsearch 0.1.0
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.
- checksums.yaml +7 -0
- data/lib/bfsearch.rb +138 -0
- data/test/test_bfsearch.rb +97 -0
- metadata +45 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 129bb56223f1aa129ca1abefda4b85b23da47c47
|
|
4
|
+
data.tar.gz: a65004653985341496fc91c1cd440657ebcb230f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e0703db610278237f10eae69ff6b073bebc898127209c5e59939ce165f85568b18f2d40e63f21c1e7f97581da731ea04caa3e5d410397124e7f67445abd50b92
|
|
7
|
+
data.tar.gz: b8dda051b7b5834263486fdc87d6b834bd393b5644361796687d0fc5450b43f5cb05a686cbdf20560392400eaeb1a1855bd3f5ae5769caf5bc05de1798753a2e
|
data/lib/bfsearch.rb
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# IDAstar --- A pure Ruby implementation of the IDA* search algorithm
|
|
2
|
+
# Copyright (C) 2016 Eric MSP Veith <eveith+idastar-ruby@veith-m.de>
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# This class implements the Best-First Search algorithm.
|
|
19
|
+
#
|
|
20
|
+
# The Best-First Search algorithm (BFS) tries to find a valid path from a
|
|
21
|
+
# starting node to a destination node. It will always find a path if one
|
|
22
|
+
# exists.
|
|
23
|
+
#
|
|
24
|
+
# The typical usage is to call BFsearch.find_path.
|
|
25
|
+
module BFsearch
|
|
26
|
+
Entry = Struct.new(:node, :parent, :distance)
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# Finds a way from +root_node+ to +destination_node+ and returns the path
|
|
30
|
+
# node by node.
|
|
31
|
+
#
|
|
32
|
+
# This method implements the Best-First Search algorithm. It starts at
|
|
33
|
+
# +root_node+, trying to find a way to the +destination+.
|
|
34
|
+
#
|
|
35
|
+
# The distance between two nodes is measured using a function object passed
|
|
36
|
+
# as the +distance_function+ parameter. It is called as:
|
|
37
|
+
#
|
|
38
|
+
# distance_function.call(node_1, node_2) # => Float
|
|
39
|
+
#
|
|
40
|
+
# Whenever the algorithm advances from the current node, it needs to know of
|
|
41
|
+
# the node's neighbors. For this, the foruth parameter,
|
|
42
|
+
# +neighbors_function+, is required. It is also assumed to be a lambda
|
|
43
|
+
# object or any object that responds to
|
|
44
|
+
# +neighbors_function#call(current_node)+ and returns a list of adjacent
|
|
45
|
+
# nodes.
|
|
46
|
+
#
|
|
47
|
+
# neighbors_function(node) # => Array
|
|
48
|
+
#
|
|
49
|
+
# BFS is an informed search algorithm, i.e., it uses an heuristic to select
|
|
50
|
+
# the probably best next node at any given state. This heuristic is
|
|
51
|
+
# implemented using a Lambda and the third parameter,
|
|
52
|
+
# +heuristic_function(node)+. The distance function must return an
|
|
53
|
+
# optimistic guess of the distance from the given node to the destination
|
|
54
|
+
# node. 'Optimistic' means that the value returned (a float) must be equal
|
|
55
|
+
# to or lower than the actual distance, but never greater. The air-line
|
|
56
|
+
# distance is a valid implementation of the distance function, since it will
|
|
57
|
+
# always return a value equal to or lower than the actual distance due to
|
|
58
|
+
# the triangle inequality.
|
|
59
|
+
#
|
|
60
|
+
# heuristic_function(node) # => Float
|
|
61
|
+
def self.find_path(root_node, destination,
|
|
62
|
+
distance_function, neighbors_function, heuristic_function)
|
|
63
|
+
root_entry = Entry.new(root_node, nil, 0.0)
|
|
64
|
+
open = [root_entry]
|
|
65
|
+
closed = []
|
|
66
|
+
|
|
67
|
+
until open.empty?
|
|
68
|
+
catch :restart do
|
|
69
|
+
current = open.shift
|
|
70
|
+
return construct_path(root_node, current) if current.node == destination
|
|
71
|
+
closed.push current
|
|
72
|
+
|
|
73
|
+
neighbors_function.call(current.node).each do |node|
|
|
74
|
+
open_entry = open.find { |i| i.node == node }
|
|
75
|
+
closed_entry = closed.find { |i| i.node == node }
|
|
76
|
+
entry = open_entry || closed_entry ||
|
|
77
|
+
Entry.new(node, current, nil)
|
|
78
|
+
|
|
79
|
+
distance_from_start = distance(entry, distance_function)
|
|
80
|
+
entry.distance ||= distance_from_start
|
|
81
|
+
|
|
82
|
+
# Greedy BFS:
|
|
83
|
+
|
|
84
|
+
heuristic_neighbor = heuristic_function.call node
|
|
85
|
+
heuristic_current = heuristic_function.call current.node
|
|
86
|
+
if heuristic_neighbor < heuristic_current
|
|
87
|
+
open.unshift current
|
|
88
|
+
open.unshift entry
|
|
89
|
+
throw :restart
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if !(open_entry || closed_entry)
|
|
93
|
+
open.push entry
|
|
94
|
+
else
|
|
95
|
+
if distance_from_start < entry.distance
|
|
96
|
+
entry.parent = current
|
|
97
|
+
entry.distance = distance_from_start
|
|
98
|
+
open.push entry unless open_entry
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
open.sort_by(&:distance)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
##
|
|
109
|
+
# Constructs and returns an array that contains the best path from the
|
|
110
|
+
# root_node to the destination object.
|
|
111
|
+
def self.construct_path(root_node, destination)
|
|
112
|
+
nodes = [destination[:node]]
|
|
113
|
+
|
|
114
|
+
while destination[:node] != root_node
|
|
115
|
+
destination = destination[:parent]
|
|
116
|
+
nodes.unshift destination[:node]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
nodes
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
##
|
|
123
|
+
# Retruns the accumulated distance between two nodes along the currently
|
|
124
|
+
# recorded path
|
|
125
|
+
def self.distance(start, distance_function)
|
|
126
|
+
current = start
|
|
127
|
+
distance = 0.0
|
|
128
|
+
|
|
129
|
+
while current.parent
|
|
130
|
+
distance += distance_function.call current.node, current.parent.node
|
|
131
|
+
current = current.parent
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
distance
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private_class_method :distance, :construct_path
|
|
138
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
require 'minitest/autorun'
|
|
2
|
+
require 'bfsearch'
|
|
3
|
+
|
|
4
|
+
class BFsearchTest < Minitest::Unit::TestCase
|
|
5
|
+
def test_smallest_tree
|
|
6
|
+
tree = {
|
|
7
|
+
name: :a,
|
|
8
|
+
next: [
|
|
9
|
+
{
|
|
10
|
+
name: :b,
|
|
11
|
+
next: [
|
|
12
|
+
{
|
|
13
|
+
name: :goal,
|
|
14
|
+
next: []
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: :c,
|
|
20
|
+
next: []
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
distance = ->(n, m) { 1.0 }
|
|
26
|
+
heuristic = ->(node) { 1.0 }
|
|
27
|
+
neighbors = ->(node) { node[:next] }
|
|
28
|
+
|
|
29
|
+
path = BFsearch.find_path(tree, tree[:next][0][:next][0],
|
|
30
|
+
distance, neighbors, heuristic)
|
|
31
|
+
assert_equal 3, path.length
|
|
32
|
+
assert_equal :goal, path[-1][:name]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_parallel_paths
|
|
36
|
+
tree = {
|
|
37
|
+
sb: {
|
|
38
|
+
i: :sb,
|
|
39
|
+
h: 222,
|
|
40
|
+
n: { kl: 70, ka: 145 }
|
|
41
|
+
},
|
|
42
|
+
wu: {
|
|
43
|
+
i: :wu,
|
|
44
|
+
h: 0,
|
|
45
|
+
n: {}
|
|
46
|
+
},
|
|
47
|
+
kl: {
|
|
48
|
+
i: :kl,
|
|
49
|
+
h: 158,
|
|
50
|
+
n: { f: 103, lu: 53 }
|
|
51
|
+
},
|
|
52
|
+
hn: {
|
|
53
|
+
i: :hn,
|
|
54
|
+
h: 87,
|
|
55
|
+
n: { wu: 102 }
|
|
56
|
+
},
|
|
57
|
+
ka: {
|
|
58
|
+
i: :ka,
|
|
59
|
+
h: 140,
|
|
60
|
+
n: { hn: 84 }
|
|
61
|
+
},
|
|
62
|
+
f: {
|
|
63
|
+
i: :f,
|
|
64
|
+
h: 96,
|
|
65
|
+
n: { wu: 116 }
|
|
66
|
+
},
|
|
67
|
+
lu: {
|
|
68
|
+
i: :lu,
|
|
69
|
+
h: 108,
|
|
70
|
+
n: { wu: 183 }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
distance = ->(n, m) { n[:n][m[:i]] || 1000.0 }
|
|
75
|
+
heuristic = ->(node) { node[:h] }
|
|
76
|
+
neighbors = ->(node) { node[:n].keys.map { |k| tree[k] } }
|
|
77
|
+
|
|
78
|
+
path = BFsearch.find_path(tree[:sb], tree[:wu],
|
|
79
|
+
distance, neighbors, heuristic)
|
|
80
|
+
assert_equal 4, path.length
|
|
81
|
+
assert_equal :sb, path[0][:i]
|
|
82
|
+
assert_equal :kl, path[1][:i]
|
|
83
|
+
assert_equal :f, path[2][:i]
|
|
84
|
+
assert_equal :wu, path[3][:i]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_map
|
|
88
|
+
map = "########" +
|
|
89
|
+
"# X #" +
|
|
90
|
+
"# #" +
|
|
91
|
+
"# ## #" +
|
|
92
|
+
"# # #" +
|
|
93
|
+
"# #" +
|
|
94
|
+
"# S #" +
|
|
95
|
+
"########"
|
|
96
|
+
end
|
|
97
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: bfsearch
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Eric MSP Veith
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2016-10-16 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: A pure Ruby implementation of gBFS
|
|
14
|
+
email: eveith+bfsearch@veith-m.de
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- lib/bfsearch.rb
|
|
20
|
+
- test/test_bfsearch.rb
|
|
21
|
+
homepage: https://github.com/eveith/bfsearch
|
|
22
|
+
licenses:
|
|
23
|
+
- GPLv3
|
|
24
|
+
metadata: {}
|
|
25
|
+
post_install_message:
|
|
26
|
+
rdoc_options: []
|
|
27
|
+
require_paths:
|
|
28
|
+
- lib
|
|
29
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '0'
|
|
39
|
+
requirements: []
|
|
40
|
+
rubyforge_project:
|
|
41
|
+
rubygems_version: 2.2.2
|
|
42
|
+
signing_key:
|
|
43
|
+
specification_version: 4
|
|
44
|
+
summary: The Greedy Best First Search algorithm
|
|
45
|
+
test_files: []
|