0

I am not a Berkeley student, I'm just taking this course for fun (so you aren't helping me cheat). I've implemented their project 1, but I am failing the autograder for Question 1 (DFS) and only question 1. I'm always skeptical of the "it's the grader that's wrong!" statements, but in this case... I think there is a mistake in the autograder :)

The following is my implementation, which fails the autograder:

def depthFirstSearch(problem):
    """
    Search the deepest nodes in the search tree first.

    Your search algorithm needs to return a list of actions that reaches the
    goal. Make sure to implement a graph search algorithm.

    To get started, you might want to try some of these simple commands to
    understand the search problem that is being passed in:
    """
    start = problem.getStartState()
    frontier = util.Stack()
    frontier.push([(start,None)])
    visited = {start}
    while frontier:
        path = frontier.pop() # path = [(pt1,dir1),(pt2,dir2)...]        
        pt0, dir0 = state = path[-1]
        if problem.isGoalState(pt0):
            p = [p[1] for p in path if p[1]] # return dirs only, not points, first dir is "None" so filter that out
            return p
        for pt1,dir1,cost1 in problem.getSuccessors(pt0):
            if pt1 not in visited:
                visited.add(pt1)
                frontier.push(path + [(pt1,dir1)])

And this code passes (identical except for the two lines marked added/removed):

def depthFirstSearch(problem):
    """
    Search the deepest nodes in the search tree first.

    Your search algorithm needs to return a list of actions that reaches the
    goal. Make sure to implement a graph search algorithm.

    To get started, you might want to try some of these simple commands to
    understand the search problem that is being passed in:
    """
    start = problem.getStartState()
    frontier = util.Stack()
    frontier.push([(start,None)])
    visited = {start}
    while frontier:
        path = frontier.pop() # path = [(pt1,dir1),(pt2,dir2)...]        
        pt0, dir0 = state = path[-1]
        if problem.isGoalState(pt0):
            p = [p[1] for p in path if p[1]] # return dirs only, not points, first dir is "None" so filter that out
            return p
        visited.add(p0) # ADDED
        for pt1,dir1,cost1 in problem.getSuccessors(pt0):
            if pt1 not in visited:
                # visited.add(pt1) # REMOVED
                frontier.push(path + [(pt1,dir1)])

The only difference between the two is when you mark a node as visited. In the latter version, you mark upon popping from the stack, then you generate neighbors, adding them to the frontier if you haven't already visited them. In the latter, you generate neighbors first, and then for each neighbor you check if they've been visited, adding them to the visited set and frontier if not.

Both versions will find the same path, and both versions are correct as I understand it. If anything, the first version is actually a better implementation because it avoids re-queuing already visited states as outlined in this answer to a similar question. You could avoid the re-queuing issue inherent in the second implementation by also checking that a state is not on the frontier before enqueuing, but that's a lot of wasted work to scan the frontier for every neighbor.

Is there some conceptual misunderstanding I've made here, or is my skepticism warranted? I've tried to dissect the autograder testcases but there is a lot of magic going on and it's tough to follow the evaluate function in autograder.py. From what I can tell though, this is the test that fails:

# Graph where BFS finds the optimal solution but DFS does not
class: "GraphSearchTest"
algorithm: "depthFirstSearch"

diagram: """
/-- B
|   ^
|   |
|  *A -->[G]
|   |     ^
|   V     |
\-->D ----/

A is the start state, G is the goal.  Arrows
mark possible transitions
"""
# The following section specifies the search problem and the solution.
# The graph is specified by first the set of start states, followed by
# the set of goal states, and lastly by the state transitions which are
# of the form: 
#      <start state> <actions> <end state> <cost>
graph: """
start_state: A
goal_states: G
A 0:A->B B 1.0
A 1:A->G G 2.0
A 2:A->D D 4.0
B 0:B->D D 8.0
D 0:D->G G 16.0
"""

and the reason it fails is that the path I find is just going straight from A->G whereas the grader wants to see A->D->G (in both cases {A,D} is the visited set). To me this would be entirely dependent on the order the neighbors are added to the fringe. If G is the final neighbor of A added to the fringe (stack here), my solution makes sense. If not it doesn't. This is where I got stuck, I couldn't tell from the test cases what order neighbors were being added in due to the metaprogramming magic.

EDIT

Shortly after posting this I found a file that allowed me to inspect the test-case that was failing, after some modification. For future learners, it's in `graphProblem.py`. After loading up the graph mentioned above using this file, and adding some print statements to my DFS, here are the results:

My version:

Step 1: Frontier = [A], Visited = {A}
Step 2: Frontier = [A->B,A->G,A->D], Visited = {A,G,D,B}
Step 3: Frontier = [A->B,A->G], Visited = {A,G,D,B}
Step 4: Goal

Between steps 2 and 3 is the critical difference. Ending Step 2 we have A->D at the top of the stack, so starting step 3 we are in State D. However, since we've recorded all of the nodes as visited as soon as we generate them as successors (in step 2), we do not add any additional paths to the frontier, and then on step 4 we pop A->G and arrive at the goal.

Contrast this with the "correct" sequence of actions the autograder requires:

Step 1: Frontier = [A], Visited = {A}
Step 2: Frontier = [A->B,A->G,A->D], Visited = {A}
Step 3: Frontier = [A->B,A->G,A->D->G], Visited = {A,D}
Step 4: Goal

Here, since we do not add G to the visited set upon generating successors in step 2 (only when de-queing do we add items to the visited set), we are able to extend path A->D to add G despite the fact that G is already on the frontier.

So, the autograder is wrong.

Solaxun
  • 2,732
  • 1
  • 22
  • 41

0 Answers0