MarissaLeigh
MarissaLeigh

Reputation: 89

Solving a maze using recursion in python

So, I have an assignment which asks me to solve a maze using recursion. I will post the assignment guidelines so you can see what I am talking about. The professor didn't explain recursion that much, he gave us examples of recursion, which I will post, but I was hoping someone might be able to give me a more in depth explanation of the recursion, and how I would apply this to solving a maze. I'm not asking for anyone to write the code, I'm just hoping some explanations would put me on the right path. Thank you to anyone who answers.

Here are the examples I have:

    def foo():
        print("Before")
        bar()
        print("After")

    def bar():
        print("During")


    def factorial(n):
        """n!"""
        product = 1
        for i in range(n,0,-1):
        product *= i
        return product

    def recFac(n):
        """n! = n * (n-1)!"""
        if(n == 1):
          return 1
        return n * recFac(n-1)

    def hello():
        """Stack overflow!"""
        hello()

    def fib(n):
        """f(n) = f(n-1) + f(n-2)
        f(0) = 0
        f(1) = 1"""
        if n == 0 or n == 1: #base case
           return n
        return fib(n-1) + fib(n-2) #recursive case

    def mult(a,b):
        """a*b = a + a + a + a ..."""
        #base case
        if (b == 1):
           return a
        #recursive case
        prod = mult(a,b-1)
        prod *= a
        return prod


    def exp(a,b):
        """a ** b = a* a * a * a * a *.... 'b times'"""
        #base case
        if (b==0):
           return 1
        if (b == 1):
           return a
        #recursive case
        return exp(a,b-1)*a

    def pallindrome(word):
        """Returns True if word is a pallindrome, False otherwise"""
        #base case
        if word == "" or len(word)==1:
           return True

        #recursive case
        if word[0] == word[len(word)-1]:
        word = word[1:len(word)-1]
        return pallindrome(word)
        else:
            return False

Here are the guidelines:

You are going to create a maze crawler capable of solving any maze you give it with the power of recursion!

Question 1 - Loading the maze

Before you can solve a maze you will have to load it. For this assignment you will use a simple text format for the maze. You may use this sample maze or create your own.

Your objective for this question is to load any given maze file, and read it into a 2-dimensional list. E.g.: loadMaze("somemaze.maze") should load the somemaze.maze file and create a list like the following...

    [['#','#','#','#','#','#','#','#','#'], 
     ['#','S','#',' ',' ',' ','#','E','#'], 
     ['#',' ','#',' ','#',' ',' ',' ','#'], 
     ['#',' ',' ',' ','#',' ','#',' ','#'], 
     ['#', #','#','#','#','#','#','#','#']] 

Note that the lists have been stripped of all '\r' and '\n' characters. In order to make the next question simpler you may make this list a global variable.

Next write a function that prints out the maze in a much nicer format:

E.g.,

    ####################################
    #S#  ##  ######## # #      #     # #
    # #   #             # #        #   #
    #   # ##### ## ###### # #######  # #
    ### # ##    ##      # # #     #### #
    #   #    #  #######   #   ###    #E#
    ####################################

Test your code with different mazes before proceeding.

Question 2 - Preparing to solve the maze

Before you can solve the maze you need to find the starting point! Add a function to your code called findStart() that will search the maze (character-by-character) and return the x and y coordinate of the 'S' character. You may assume that at most one such character exists in the maze. If no 'S' is found in the maze return -1 as both the x and y coordinates.

Test your code with the 'S' in multiple locations (including no location) before proceeding.

Question 3 - Solving the maze!

Finally, you are ready to solve the maze recursively! Your solution should only require a single method: solve(y,x)

A single instance of the solve method should solve a single location in your maze. The parameters y and x are the current coordinates to be solved. There are a few things your solve method should accomplish. It should check if it is currently solving the location of the 'E'. In that case your solve method has completed successfully. Otherwise it should try to recursively solve the space to the right. Note, your method should only try to solve spaces, not walls ('#'). If that recursion doesn't lead to the end, then try down, then left, and up. If all that fails, your code should backtrack a step, and try another direction.

Lastly, while solving the maze, your code should leave indicators of its progress. If it is searching to the right, the current location should have a '>' in place of the empty space. If searching down put a 'v'. If searching left '<', and if searching up '^'. If your code has to backtrack remove the direction arrow, and set the location back to a ' '.

Once your maze is solved print out the maze again. You should a see step-by-step guide to walking the maze. E.g.,

    main("somemaze.maze")
    ######### 
    #S#   #E# 
    # # #   # 
    #   # # # 
    #########

S is at (1,1)

     ######### 
     #S#>>v#E# 
     #v#^#>>^# 
     #>>^# # # 
     #########

Test your code with different different start and end locations, and optionally over a variety of mazes.

Here is the code I have so far: But the code is not actually printing the track in the maze, and I'm not sure why.

    def loadMaze():
        readIt = open('Maze.txt', 'r')
        readLines = readIt.readlines()
        global mazeList
        mazeList = [list(i.strip()) for i in readLines]

    def showMaze():
        for i in mazeList:
            mazeprint = ''
        for j in i:
            mazeprint = mazeprint + j
        print(mazeprint)
        print('\n')    

    def solve(x,y, mazeList):
        mazeList[x][y] = "o"
        #Base case  
        if y > len(mazeList) or x > len(mazeList[y]):
           return False
        if mazeList[y][x] == "E":
           return True 
        if mazeList[y][x] != " ":
           return False
        #marking
        if solve(x+1,y) == True:  #right
           mazeList[x][y]= '>'
        elif solve(x,y+1) == True:  #down
             mazeList[x][y]= 'v'     
        elif solve(x-1,y) == True:  #left
             mazeList[x][y]= '<'     
        elif solve(x,y-1) == True:  #up
             mazeList[x][y]= '^'
        else:
           mazeList[x][y]= ' '
        return (mazeList[x][y]!= ' ')

Upvotes: 3

Views: 32306

Answers (5)

Max Eisenhardt
Max Eisenhardt

Reputation: 462

import os

class Maze_Crawler:
    def __init__(self):
        self.maze = []
        
    def load_maze(self, path):
        
        rows = []
        
        with open(path, 'r') as f:
            rows = f.readlines()
        
        for i in range(len(rows)):
            self.maze.append([])
        
            for j in range(len(rows[i])-1):
                self.maze[i].append(rows[i][j])
        
        return self.maze
    
    def get_start_coor(self):
        for i in range(len(self.maze)):
            for j in range(len(self.maze[i])):
                if self.maze[i][j] == 'S':
                    return i, j
        return -1, -1
    
    def solve_maze(self, coor):
        x, y = coor
        
        if self.maze[x][y] == '#' or self.maze[x][y] == 'X':
            return False
        
        if self.maze[x][y] == 'E':
            return True
        
        
        if self.maze[x][y] != 'S':
            self.maze[x][y] = 'X'
        
        if self.solve_maze((x+1, y)):
            if self.maze[x][y] != 'S':
                self.maze[x][y] = 'v'
            
        elif self.solve_maze((x-1, y)):
            if self.maze[x][y] != 'S':
                self.maze[x][y] = '^'
            
        elif self.solve_maze((x, y+1)):
            if self.maze[x][y] != 'S':
                self.maze[x][y] = '>'
            
        elif self.solve_maze((x, y-1)):
            if self.maze[x][y] != 'S':
                self.maze[x][y] = '<'
        else:
            return False
        
        return True
      
    def show_solution(self):
        for i in range(len(self.maze)):
            r = ''
            for j in range(len(self.maze[i])):
                if self.maze[i][j] == 'X':
                    r +=  ' '
                else:
                    r += self.maze[i][j]
            print(r)

Upvotes: 1

Ihor Pomaranskyy
Ihor Pomaranskyy

Reputation: 5651

Here is my solution of CodeEval's The Labirynth challenge:

import sys
sys.setrecursionlimit(5000)


class Maze(object):
    FLOOR = ' '
    WALLS = '*'
    PATH = '+'

    def __init__(self):
        self.cols = 0
        self.rows = 0
        self.maze = []

    def walk_forward(self, current_k, r, c):
        self.maze[r][c] = current_k
        next_k = current_k + 1
        # up
        if r > 1:
            up = self.maze[r - 1][c]
            if up != self.WALLS:
                if up == self.FLOOR or int(up) > current_k:
                    self.walk_forward(next_k, r - 1, c)
        # down
        if r < self.rows - 1:
            down = self.maze[r + 1][c]
            if down != self.WALLS:
                if down == self.FLOOR or int(down) > current_k:
                    self.walk_forward(next_k, r + 1, c)
        # left
        if c > 1:
            left = self.maze[r][c - 1]
            if left != self.WALLS:
                if left == self.FLOOR or int(left) > current_k:
                    self.walk_forward(next_k, r, c - 1)
        # right
        if c < self.cols - 1:
            right = self.maze[r][c + 1]
            if right != self.WALLS:
                if right == self.FLOOR or int(right) > current_k:
                    self.walk_forward(next_k, r, c + 1)

    def walk_backward(self, r, c):
        current_k = self.maze[r][c]
        if not isinstance(current_k, int):
            return False
        self.maze[r][c] = self.PATH

        up = self.maze[r - 1][c] if r > 0 else None
        down = self.maze[r + 1][c] if r < self.rows - 1 else None
        left = self.maze[r][c - 1] if c > 1 else None
        right = self.maze[r][c + 1] if c < self.cols else None

        passed = False
        if up and isinstance(up, int) and up == current_k - 1:
            self.walk_backward(r - 1, c)
            passed = True
        if down and isinstance(down, int) and down == current_k - 1:
            self.walk_backward(r + 1, c)
            passed = True
        if left and isinstance(left, int) and left == current_k - 1:
            self.walk_backward(r, c - 1)
            passed = True
        if right and isinstance(right, int) and right == current_k - 1:
            self.walk_backward(r, c + 1)                    

    def cleanup(self, cleanup_path=False):
        for r in range(0, self.rows):
            for c in range(0, self.cols):
                if isinstance(self.maze[r][c], int):
                    self.maze[r][c] = self.FLOOR
                if cleanup_path and self.maze[r][c] == self.PATH:
                    self.maze[r][c] = self.FLOOR

    def solve(self, start='up', show_path=True):
        # finding start and finish points
        upper = lower = None
        for c in range(0, self.cols):
            if self.maze[0][c] == self.FLOOR:
                upper = (0, c)
                break
        for c in range(0, self.cols):
            if self.maze[self.rows - 1][c] == self.FLOOR:
                lower = (self.rows - 1, c)
                break
        if start == 'up':
            start = upper
            finish = lower
        else:
            start = lower
            finish = upper

        self.cleanup(cleanup_path=True)
        self.walk_forward(1, start[0], start[1])
        length = self.maze[finish[0]][finish[1]]
        if not isinstance(length, int):
            length = 0
        if show_path:
            self.walk_backward(finish[0], finish[1])
            self.cleanup(cleanup_path=False)
        else:
            self.cleanup(cleanup_path=True)
        return length

    def save_to_file(self, filename):
        with open(filename, 'w') as f:
            f.writelines(str(self))

    def load_from_file(self, filename):
        self.maze = []
        with open(filename, 'r') as f:
            lines = f.readlines()
        for line in lines:
            row = []
            for c in line.strip():
                row.append(c)
            self.maze.append(row)
        self.rows = len(self.maze)
        self.cols = len(self.maze[0]) if self.rows > 0 else 0

    def get_maze(self):
        return copy.copy(self.maze)

    def __str__(self):
        as_string = u''
        for row in self.maze:
            as_string += u''.join([str(s)[-1] for s in row]) + "\n"
        return as_string


maze = Maze()
maze.load_from_file(sys.argv[1])
maze.solve(show_path=True)
print str(maze)

Upvotes: 2

Hugh Bothwell
Hugh Bothwell

Reputation: 56694

Recursion is actually a simple idea: to solve a problem, you shrink the problem by one step, then solve the reduced problem. This continues until you reach a "base problem" that you know how to solve completely. You return the base solution, then add to the solution returned at each step until you have the full solution.

So to solve n!, we remember n and solve for (n-1)!. The base case is 1!, for which we return 1; then at each return step we multiply by the remembered number (2 * 1! is 2, 3 * 2! is 6, 4 * 3! is 24, 5 * 4! is 120) until we multiply by n and have the full solution. This is actually a pretty pale and anemic sort of recursion; there is only one possible decision at each step. Known as "tail recursion", this is very easy to turn inside-out and convert to an iterative solution (start at 1 and multiply by each number up to n).

A more interesting sort of recursion is where you split the problem in half, solve each half, then combine the two half-solutions; for example quicksort sorts a list by picking one item, dividing the list into "everything smaller than item" and "everything bigger than item", quicksorting each half, then returning quicksorted(smaller) + item + quicksorted(larger). The base case is "when my list is only one item, it is sorted".

For the maze, we are going to split the problem four ways - all solutions possible if I go right, left, up, and down from my current location - with the special feature that only one of the recursive searches will actually find a solution. The base case is "I am standing on E", and a failure is "I am in a wall" or "I am on a space I have already visited".


Edit: for interest's sake, here is an OO solution (compatible with both Python 2.x and 3.x):

from collections import namedtuple

Dir = namedtuple("Dir", ["char", "dy", "dx"])

class Maze:
    START = "S"
    END   = "E"
    WALL  = "#"
    PATH  = " "
    OPEN  = {PATH, END}  # map locations you can move to (not WALL or already explored)

    RIGHT = Dir(">",  0,  1)
    DOWN  = Dir("v",  1,  0)
    LEFT  = Dir("<",  0, -1)
    UP    = Dir("^", -1,  0)
    DIRS  = [RIGHT, DOWN, LEFT, UP]

    @classmethod
    def load_maze(cls, fname):
        with open(fname) as inf:
            lines = (line.rstrip("\r\n") for line in inf)
            maze  = [list(line) for line in lines]
        return cls(maze)

    def __init__(self, maze):
        self.maze = maze

    def __str__(self):
        return "\n".join(''.join(line) for line in self.maze)

    def find_start(self):
        for y,line in enumerate(self.maze):
            try:
                x = line.index("S")
                return y, x
            except ValueError:
                pass

        # not found!
        raise ValueError("Start location not found")

    def solve(self, y, x):
        if self.maze[y][x] == Maze.END:
            # base case - endpoint has been found
            return True
        else:
            # search recursively in each direction from here
            for dir in Maze.DIRS:
                ny, nx = y + dir.dy, x + dir.dx
                if self.maze[ny][nx] in Maze.OPEN:  # can I go this way?
                    if self.maze[y][x] != Maze.START: # don't overwrite Maze.START
                        self.maze[y][x] = dir.char  # mark direction chosen
                    if self.solve(ny, nx):          # recurse...
                        return True                 # solution found!

            # no solution found from this location
            if self.maze[y][x] != Maze.START:       # don't overwrite Maze.START
                self.maze[y][x] = Maze.PATH         # clear failed search from map
            return False

def main():
    maze = Maze.load_maze("somemaze.txt")

    print("Maze loaded:")
    print(maze)

    try:
        sy, sx = maze.find_start()
        print("solving...")
        if maze.solve(sy, sx):
            print(maze)
        else:
            print("    no solution found")
    except ValueError:
        print("No start point found.")

if __name__=="__main__":
    main()

and when run produces:

Maze loaded:
    ####################################
    #S#  ##  ######## # #      #     # #
    # #   #             # #        #   #
    #   # ##### ## ###### # #######  # #
    ### # ##    ##      # # #     #### #
    #   #    #  #######   #   ###    #E#
    ####################################
solving...
    ####################################
    #S#  ##  ######## # #>>>>>v#  >>v# #
    #v#>>v#    >>>v     #^#   >>>>^#>>v#
    #>>^#v#####^##v######^# #######  #v#
    ### #v##>>>^##>>>>>v#^# #     ####v#
    #   #>>>^#  #######>>^#   ###    #E#
    ####################################

Note that the assignment as given has a few unPythonic elements:

  • it asks for camelCase function names rather than underscore_separated
  • it suggests using a global variable rather than passing data explicitly
  • it asks for find_start to return flag values on failure rather than raising an exception

Upvotes: 0

sabbahillel
sabbahillel

Reputation: 4425

Maze solving with python shows my answer. However, if you want to do the code yourself the steps are.

 1. Start at the entrance.  
 2. Call the function solve(x,y) with the entrance co-ordinates  
 3. in solve, return false if the input point has already been handled or is a wall.  
 4. Mark the current point as handled (tag = 'o')  
 5. go to the right and call solve on that point. If it returns true, set tag to '>'  
 6 elif do the same for left and '<'  
 7 elif do the same for up and '^'  
 8 elif do the same for down and 'v'  
 9 else this is a false path, set tag = ' ' 
10 set the current maze point to tag
11 return (tag != ' ')

Alternatively leave step 9 out and make step 11

return(tag != 'o')

Then search through the maze and replace every 'o' with ' '

You can display the maze both ways so that it will show how you tried to solve it as well as the final answer. This has been used as a Solaris screensaver with the potential paths showing in one color and the actual path in a different color so that you can see it trying and then succeeding.

Upvotes: 0

EdwinW
EdwinW

Reputation: 1036

(Dating myself, I actually did this problem in COBOL, in high-school.)

You can think of solving the maze as taking steps.

When you take a step, the same rules apply every time. Because the same rules apply every time, you can use the exact same set of instructions for each step. When you take a step, you just call the same routine again, changing the parameters to indicate the new step. That's recursion. You break the problem down by taking it one step at a time.

Note: Some recursion solutions break the problem in half, solving each half independent of the other, that works when the two solutions are actually independent. It doesn't work here because each step (solution) depends on the previous steps.

If you hit a dead end, you back out of the dead end, until you find a step where there are still viable squares to check.

Helpful Hint: You don't mark the correct path on the way to the exit, because you don't know that the step you're taking right now is part of the path to the exit. You mark the path on the way back, when you know that each step is indeed part of the path. You can do this because each step remembers which square it was in before it took the next step.

Instead, you put a mark in each square you've tried that only says: I've been here, no need to check this one again. Clean those up before you print the solution.

Upvotes: 2

Related Questions