Thanks to visit codestin.com
Credit goes to github.com

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)