Skip to content

Edit stdlib2.rst: Tools for Working with Lists: edit implementation of breadth_first_search() #111925

Closed
zipperer wants to merge 2 commits into
python:mainfrom
zipperer:patch-3
Closed

Edit stdlib2.rst: Tools for Working with Lists: edit implementation of breadth_first_search() #111925
zipperer wants to merge 2 commits into
python:mainfrom
zipperer:patch-3

Conversation

@zipperer

@zipperer zipperer commented Nov 9, 2023

Copy link
Copy Markdown
Contributor

I claim: the implementation of breadth_first_search is incorrect.

Argument 1:
The search does not check whether is_goal(starting_node).
So, the implementation will fail to return starting_node when is_goal(starting_node).

Argument 2:
The search stops after it checks and enqueues each child of starting_node. Put differently: the search enqueues each child of starting_node but does not proceed to apply gen_moves to each child.
So, the implementation will fail to return a goal if that goal has depth > 1.

This commit proposes two changes:

  • check whether is_goal(starting_node)
  • use while so search continues after it enqueues each child of starting_node
    • this permits search beyond depth = 1

This commit does not propose adding a set for visited nodes, because the example specifies tree searches rather than graph searches: "These objects are well suited for implementing queues and breadth first tree searches...".


📚 Documentation preview 📚: https://cpython-previews--111925.org.readthedocs.build/

I claim the implementation of `breadth_first_search` is incorrect because:
 - the search does not check whether `is_goal(starting_node)`
 - the search stops after it checks+enqueues each child of `starting_node`
 -- put differently: the search enqueues each child of `starting_node` but does not proceed to apply `gen_moves` to each child

So, the implementation will fail to return `starting_node` when `is_goal(starting_node)` and it will fail to return a goal if that goal has depth > 1.

This commit proposes two changes:
 - check whether `is_goal(starting_node)`
 - use `while` so search continues after it enqueues children of `starting_node`

This commit does not propose adding a set for visited nodes, because the example specifies tree searches rather than graph searches: "These objects are well suited for implementing queues and breadth first tree searches...".
@bedevere-app bedevere-app Bot added awaiting review docs Documentation in the Doc dir skip news labels Nov 9, 2023
@zipperer

zipperer commented Nov 9, 2023

Copy link
Copy Markdown
Contributor Author

Before and after this commit, the implementation takes it as given that: when breadth_first_search(unsearched) returns None, the search failed to find a goal node.

@zipperer

zipperer commented Nov 9, 2023

Copy link
Copy Markdown
Contributor Author

Further, the implementation in this commit relies on an empty deque evaluating to false in a boolean context. (https://docs.python.org/3/library/stdtypes.html#truth-value-testing). That is,

>>> from collections import deque
>>> empty_deque = deque()

>>> if empty_deque:
...     print('empty_deque treated as True')
... else:
...     print('empty_deque treated as False')
...
empty_deque treated as False

@serhiy-storchaka

Copy link
Copy Markdown
Member

You think that breadth_first_search() is incorrect because it does not do what you think it should do, but in fact it is correct, but it just doesn't do what you think it should do.

It does not check is_goal(starting_node) because starting_node is a starting position, it is not a valid move.

It only implements a single step of the search. This is why it takes unsearched as argument instead of starting_node. You can call it repeatedly to get more results.

For example take the eight queens puzzle. The starting_node is an empty chessboard. gen_moves() produces positions with added queen that does not share the same row, column, or diagonal with other queens. is_goal() checks that all 8 quins are placed (or that there is no place for a new queen). By calling breadth_first_search() repeatedly you can get all 92 solutions.

cc @rhettinger

@zipperer

Copy link
Copy Markdown
Contributor Author

Hey, @serhiy-storchaka

Thank you for your reply! It is very helpful.

You think that breadth_first_search() is incorrect because it does not do what you think it should do, but in fact it is correct, but it just doesn't do what you think it should do.

I agree. From the name I inferred the intent is to perform breadth first search through the entire space of nodes -- either returning a goal node when found or failing after having visited every node. But there is no evidence (e.g. a docstring, test, comment, or surrounding text) that states that is the intent. So, I inferred more than what is stated.

The intent of breadth_first_search() may be strictly to provide an example of using a deque(). Showing the steps of the search that make a deque() and use it while expanding one node suffices for that purpose. It is not necessary to implement the whole search in the example.

Given that:

  • I inferred more than what is stated and
  • this pull request is based on what I inferred to be the intent of breadth_first_search(), and
  • the intent of breadth_first_search() likely differs from what I inferred

we can close the pull request and mark it invalid.


Additional notes

It only implements a single step of the search. ... You can call it repeatedly to get more results.

I agree it implements only a single step of the search and that it can be called repeatedly to possibly return a result or enqueue more nodes.

The version in the commit on this pull request executes the body repeatedly in the while.

This is another version of how I was thinking about it:

def expand_next_unsearched_node(unsearched): # -> Optional[Node]; modifies input unsearched
    node = unsearched.popleft()
    if is_goal(node):
        return node
    for m in gen_moves(node):
        unsearched.append(m)

def breadth_first_search(starting_node): # -> Optional[Node]
    unsearched = deque([starting_node])
    while unsearched:
        goal_node_or_none = expand_next_unsearched_node(unsearched)
        if goal_node_or_none is not None:
            return goal_node_or_none

where the version in the docs corresponds to expand_next_unsearched_node(unsearched).

This is why it takes unsearched as argument instead of starting_node.

I think I see why that suggests the intent of breadth_first_search() is to perform one step. If breadth_first_search() took the starting_node as input, then it would be responsible for initializing the deque() and performing the whole search. Because breadth_first_search() takes unsearched instead, breadth_first_search() can check the current state, perform one step of the work, possibly update the state, and return control without completing the search.

It does not check is_goal(starting_node) because starting_node is a starting position, it is not a valid move. ...
For example take the eight queens puzzle. The starting_node is an empty chessboard. gen_moves() produces positions with added queen that does not share the same row, column, or diagonal with other queens. is_goal() checks that all 8 quins are placed (or that there is no place for a new queen). By calling breadth_first_search() repeatedly you can get all 92 solutions.

I agree that in the example you provide the starting_node will not satisfy is_goal.

Thank you again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting review docs Documentation in the Doc dir skip news

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants