Skip to content

Commit 06a94a2

Browse files
committed
Fixed search bug reported by Matthew Yurka, and others:
* graph_search missed deduping nodes in the frontier * astar_search didn't use 3rd-edition pseudocode * InstrumentedProblem didn't forward the path_cost method, so all the doctest results on instrumented cost-sensitive problems were wrong Also filled in breadth_first_search and per-node memoization of h (to fulfill the doc comment saying that was done). There's a remaining problem that sometimes trivial refactorings change the amount of search that is done, slightly, which should not happen since the code is supposed to be deterministic.
1 parent 081f6fa commit 06a94a2

File tree

2 files changed

+101
-43
lines changed

2 files changed

+101
-43
lines changed

search.py

Lines changed: 90 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,17 @@ def path(self):
9797
node = node.parent
9898
return list(reversed(path_back))
9999

100+
# We want for a queue of nodes in breadth_first_search or
101+
# astar_search to have no duplicated states, so we treat nodes
102+
# with the same state as equal. [Problem: this may not be what you
103+
# want in other contexts.]
104+
105+
def __eq__(self, other):
106+
return isinstance(other, Node) and self.state == other.state
107+
108+
def __hash__(self):
109+
return hash(self.state)
110+
100111
#______________________________________________________________________________
101112

102113
class SimpleProblemSolvingAgentProgram:
@@ -143,7 +154,7 @@ def tree_search(problem, frontier):
143154
def graph_search(problem, frontier):
144155
"""Search through the successors of a problem to find a goal.
145156
The argument frontier should be an empty queue.
146-
If two paths reach a state, only use the best one. [Fig. 3.7]"""
157+
If two paths reach a state, only use the first one. [Fig. 3.7]"""
147158
frontier.append(Node(problem.initial))
148159
explored = set()
149160
while frontier:
@@ -153,7 +164,7 @@ def graph_search(problem, frontier):
153164
explored.add(node.state)
154165
frontier.extend(child for child in node.expand(problem)
155166
if child.state not in explored
156-
and child.state not in frontier)
167+
and child not in frontier)
157168
return None
158169

159170
def breadth_first_tree_search(problem):
@@ -164,21 +175,61 @@ def depth_first_tree_search(problem):
164175
"Search the deepest nodes in the search tree first."
165176
return tree_search(problem, Stack())
166177

167-
def breadth_first_graph_search(problem):
168-
"Search the shallowest nodes in the search tree first."
169-
return graph_search(problem, FIFOQueue())
170-
171178
def depth_first_graph_search(problem):
172179
"Search the deepest nodes in the search tree first."
173180
return graph_search(problem, Stack())
174181

175182
def breadth_first_search(problem):
176-
"Fig. 3.11"
177-
unimplemented()
183+
"[Fig. 3.11]"
184+
node = Node(problem.initial)
185+
if problem.goal_test(node.state):
186+
return node
187+
frontier = FIFOQueue()
188+
frontier.append(node)
189+
explored = set()
190+
while frontier:
191+
node = frontier.pop()
192+
explored.add(node.state)
193+
for child in node.expand(problem):
194+
if child.state not in explored and child not in frontier:
195+
if problem.goal_test(child.state):
196+
return child
197+
frontier.append(child)
198+
return None
199+
200+
def best_first_graph_search(problem, f):
201+
"""Search the nodes with the lowest f scores first.
202+
You specify the function f(node) that you want to minimize; for example,
203+
if f is a heuristic estimate to the goal, then we have greedy best
204+
first search; if f is node.depth then we have breadth-first search.
205+
There is a subtlety: the line "f = memoize(f, 'f')" means that the f
206+
values will be cached on the nodes as they are computed. So after doing
207+
a best first search you can examine the f values of the path returned."""
208+
f = memoize(f, 'f')
209+
node = Node(problem.initial)
210+
if problem.goal_test(node.state):
211+
return node
212+
frontier = PriorityQueue(min, f)
213+
frontier.append(node)
214+
explored = set()
215+
while frontier:
216+
node = frontier.pop()
217+
if problem.goal_test(node.state):
218+
return node
219+
explored.add(node.state)
220+
for child in node.expand(problem):
221+
if child.state not in explored and child not in frontier:
222+
frontier.append(child)
223+
elif child in frontier:
224+
incumbent = frontier[child]
225+
if f(child) < f(incumbent):
226+
del frontier[incumbent]
227+
frontier.append(child)
228+
return None
178229

179230
def uniform_cost_search(problem):
180-
"Fig. 3.14"
181-
unimplemented()
231+
"[Fig. 3.14]"
232+
return best_first_graph_search(problem, lambda node: node.path_cost)
182233

183234
def depth_limited_search(problem, limit=50):
184235
"[Fig. 3.17]"
@@ -210,35 +261,22 @@ def iterative_deepening_search(problem):
210261
#______________________________________________________________________________
211262
# Informed (Heuristic) Search
212263

213-
def best_first_graph_search(problem, f):
214-
"""Search the nodes with the lowest f scores first.
215-
You specify the function f(node) that you want to minimize; for example,
216-
if f is a heuristic estimate to the goal, then we have greedy best
217-
first search; if f is node.depth then we have breadth-first search.
218-
There is a subtlety: the line "f = memoize(f, 'f')" means that the f
219-
values will be cached on the nodes as they are computed. So after doing
220-
a best first search you can examine the f values of the path returned."""
221-
f = memoize(f, 'f')
222-
return graph_search(problem, PriorityQueue(min, f))
223-
224264
greedy_best_first_graph_search = best_first_graph_search
225265
# Greedy best-first search is accomplished by specifying f(n) = h(n).
226266

227267
def astar_search(problem, h=None):
228268
"""A* search is best-first graph search with f(n) = g(n)+h(n).
229-
You need to specify the h function when you call astar_search.
230-
Uses the pathmax trick: f(n) = max(f(n), g(n)+h(n))."""
231-
h = h or problem.h
232-
def f(n):
233-
return max(getattr(n, 'f', -infinity), n.path_cost + h(n))
234-
return best_first_graph_search(problem, f)
269+
You need to specify the h function when you call astar_search, or
270+
else in your Problem subclass."""
271+
h = memoize(h or problem.h, 'h')
272+
return best_first_graph_search(problem, lambda n: n.path_cost + h(n))
235273

236274
#______________________________________________________________________________
237275
# Other search algorithms
238276

239277
def recursive_best_first_search(problem, h=None):
240278
"[Fig. 3.26]"
241-
h = h or problem.h
279+
h = memoize(h or problem.h, 'h')
242280

243281
def RBFS(problem, node, flimit):
244282
if problem.goal_test(node.state):
@@ -768,17 +806,25 @@ def goal_test(self, state):
768806
self.found = state
769807
return result
770808

809+
def path_cost(self, c, state1, action, state2):
810+
return self.problem.path_cost(c, state1, action, state2)
811+
812+
def value(self, state):
813+
return self.problem.value(state)
814+
771815
def __getattr__(self, attr):
772816
return getattr(self.problem, attr)
773817

774818
def __repr__(self):
775819
return '<%4d/%4d/%4d/%s>' % (self.succs, self.goal_tests,
776820
self.states, str(self.found)[:4])
777821

778-
def compare_searchers(problems, header, searchers=[breadth_first_tree_search,
779-
breadth_first_graph_search, depth_first_graph_search,
780-
iterative_deepening_search, depth_limited_search,
781-
astar_search, recursive_best_first_search]):
822+
def compare_searchers(problems, header,
823+
searchers=[breadth_first_tree_search,
824+
breadth_first_search, depth_first_graph_search,
825+
iterative_deepening_search,
826+
depth_limited_search, astar_search,
827+
recursive_best_first_search]):
782828
def do(searcher, problem):
783829
p = InstrumentedProblem(problem)
784830
searcher(p)
@@ -789,14 +835,14 @@ def do(searcher, problem):
789835
def compare_graph_searchers():
790836
"""Prints a table of results like this:
791837
>>> compare_graph_searchers()
792-
Searcher Romania(A, B) Romania(O, N) Australia
793-
breadth_first_tree_search < 21/ 22/ 59/B> <1158/1159/3288/N> < 7/ 8/ 22/WA>
794-
breadth_first_graph_search < 11/ 12/ 28/B> < 33/ 34/ 76/N> < 6/ 7/ 19/WA>
795-
depth_first_graph_search < 9/ 10/ 23/B> < 16/ 17/ 39/N> < 4/ 5/ 13/WA>
796-
iterative_deepening_search < 11/ 33/ 31/B> < 656/1815/1812/N> < 3/ 11/ 11/WA>
797-
depth_limited_search < 54/ 65/ 185/B> < 387/1012/1125/N> < 50/ 54/ 200/WA>
798-
astar_search < 3/ 4/ 9/B> < 8/ 9/ 22/N> < 2/ 3/ 6/WA>
799-
recursive_best_first_search < 200/ 201/ 601/B> < 71/ 72/ 213/N> < 11/ 12/ 43/WA>"""
838+
Searcher Romania(A, B) Romania(O, N) Australia
839+
breadth_first_tree_search < 21/ 22/ 59/B> <1158/1159/3288/N> < 7/ 8/ 22/WA>
840+
breadth_first_search < 7/ 11/ 18/B> < 19/ 20/ 45/N> < 2/ 6/ 8/WA>
841+
depth_first_graph_search < 8/ 9/ 20/B> < 16/ 17/ 38/N> < 4/ 5/ 11/WA>
842+
iterative_deepening_search < 11/ 33/ 31/B> < 656/1815/1812/N> < 3/ 11/ 11/WA>
843+
depth_limited_search < 54/ 65/ 185/B> < 387/1012/1125/N> < 50/ 54/ 200/WA>
844+
astar_search < 5/ 7/ 15/B> < 16/ 18/ 40/N> < 2/ 4/ 6/WA>
845+
recursive_best_first_search < 5/ 6/ 15/B> <5887/5888/16532/N> < 11/ 12/ 43/WA>"""
800846
compare_searchers(problems=[GraphProblem('A', 'B', romania),
801847
GraphProblem('O', 'N', romania),
802848
GraphProblem('Q', 'WA', australia)],
@@ -808,10 +854,12 @@ def compare_graph_searchers():
808854
>>> ab = GraphProblem('A', 'B', romania)
809855
>>> breadth_first_tree_search(ab).solution()
810856
['S', 'F', 'B']
811-
>>> breadth_first_graph_search(ab).solution()
857+
>>> breadth_first_search(ab).solution()
812858
['S', 'F', 'B']
859+
>>> uniform_cost_search(ab).solution()
860+
['S', 'R', 'P', 'B']
813861
>>> depth_first_graph_search(ab).solution()
814-
['T', 'L', 'M', 'D', 'C', 'R', 'S', 'F', 'B']
862+
['T', 'L', 'M', 'D', 'C', 'P', 'B']
815863
>>> iterative_deepening_search(ab).solution()
816864
['S', 'F', 'B']
817865
>>> len(depth_limited_search(ab).solution())

utils.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,8 @@ def __contains__(self, item):
708708
class PriorityQueue(Queue):
709709
"""A queue in which the minimum (or maximum) element (as determined by f and
710710
order) is returned first. If order is min, the item with minimum f(x) is
711-
returned first; if order is max, then it is the item with maximum f(x)."""
711+
returned first; if order is max, then it is the item with maximum f(x).
712+
Also supports dict-like lookup."""
712713
def __init__(self, order=min, f=lambda x: x):
713714
update(self, A=[], order=order, f=f)
714715
def append(self, item):
@@ -722,6 +723,15 @@ def pop(self):
722723
return self.A.pop()[1]
723724
def __contains__(self, item):
724725
return some(lambda (_, x): x == item, self.A)
726+
def __getitem__(self, key):
727+
for _, item in self.A:
728+
if item == key:
729+
return item
730+
def __delitem__(self, key):
731+
for i, (value, item) in enumerate(self.A):
732+
if item == key:
733+
self.A.pop(i)
734+
return
725735

726736
## Fig: The idea is we can define things like Fig[3,10] later.
727737
## Alas, it is Fig[3,10] not Fig[3.10], because that would be the same

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy