In general, a recursive function or method is one that makes a call to itself. In class, the recursive algorithms that we saw were generally structured as follows:
Check if the subproblem being solved for is small enough to solve directly (i.e., is it a 'base case'). If it is, return the solution to the subproblem.
Otherwise recursively call the algorithm on a smaller version of the problem, and then use the solution to the smaller problem to determine the solution to the original problem.
This approach works well for problems whose solutions can be expressed in terms of solutions to smaller subproblems. For example, a recursive algorithm for calculating n! (n factorial) easily follows from the fact that n! = n*(n-1)! when n>0 and n! = 1 when n=0.
Recursion is also typically used by algorithms that employ backtracking. We introduce the
idea of backtracking by way of an example.
Consider the following problem. You are at the entrance to a large maze that you need to find your way through. Initially, there's two directions you can travel in the maze: either left or right. If you choose to go right, then the subproblem you have to solve is how to find your way out of the maze having gone right in the initial step; if you choose to go left, then the subproblem you have to solve is how to find your way out of the maze having gone left in the initial step. At each junction in the maze you have to choose some direction to go. After making a choice at each junction, you have a smaller subproblem to solve: how to get out of the maze having made the choices you've made thus far. But there's a catch. What if you made a choice that leads you to a dead end? That is, you may end up trying to solve a "subproblem" that has no solution. If you're lucky, this won't happen and each choice you make will get you closer to the maze's exit. If you're unlucky, then you'll have to return to a junction point that you previously visited and make a different choice for which direction to go. In other words, you will have to backtrack.
Suppose we want to write a
Python function to help us find a path through the maze. How can
we use recursion in our function to do the backtracking described above? Lets call our function In the context of the maze
problem and the description we gave above, the term 'backtracking'
has a taken on a very specific meaning: you are physically backtracking
to a
previous position in the maze because a choice you made led
you to a dead end. More generally, backtracking is a technique
for systematically exploring the set
of all possible solutions to a problem. In the maze problem, for
example, the set of all possible solutions is the set of all
sequences of choices that can be made at junctions. Although The problem that you need to solve in this assignment shares some similarities with
finding a path through a maze, but it is ultimately more complex. You are trapped in an n-dimensional universe, where n is a positive integer, and you have to find your way out. The universe is divided into a grid of k1 x k2 x ... x kn cells, and each cell's position in the universe can be described by an n-element tuple containing only non-negative integers. The origin of the universe is at (0,0,...,0) (a tuple containing n 0's). In general, a cell located at position (i1,i2,...,in) is i1 cells from the origin along the first dimension, i2 cells from the origin along the second dimension, and so forth.
Two cells (i1,i2,...,in) and (i'1,i'2,...,i'n) in the universe are orthogonally adjacent if there exists some j such that either ij = i'j + 1 or ij = i'j - 1, and all other coordinates are equal. For example, in a two dimensional universe, two cells are orthogonally adjacent if one cell is left, right, above, or below the other cell. Cells that are touching but on a "diagonal" from each other are not orthogonally adjacent. (See the examples section below.)
Your movement in the universe is constrained by the following rules:
mazeSolve(path)
. This function
returns True if there exists a way out of the maze
using the sequence of choices that's been made thus far (stored in path
), or False otherwise.
A very high-level
implementation of the mazeSolve function is as follows.
def mazeSolve(path):
There are a lot of details omitted from the above description, but it
should give you an idea of how recursion can be used to do
backtracking. In particular, each recursive call to mazeSolve advances
to the next junction in the maze, and whenever a call to mazeSolve
returns, it backtracks to an earlier position in the maze. path
leads to a
dead end: return False
return True
path
path
variable
(that is, call mazeSolve(path)
)
return True
to the caller
mazeSolve
explores this set systematically until it hits upon a correct
solution.
mazeSolve
stops after finding one correct
solution, there may be other ways through the maze. One can modify the
technique given above to find all
possible ways through the maze. Think about how you might do this and
what changes you would need to make. You will need to do something
similar in the problem explained below.
The Problem – Escaping an n-Dimensional Universe
The universe contains obstacles that you must handle. In particular, a cell possibly contains one of the following:
It probably does not make sense to you that a silver troll will give you a gold bar in exchange for an extra silver bar, since gold is traditionally more valuable than silver. However, nothing in this universe makes sense, which is the reason you want to escape it.
You start at some cell in the universe with some number of gold and silver bars in your possesion. The start cell does not contain any obstacles (i.e., it is empty). To escape the universe you must reach an "escape" cell. Such a cell may or may not exist in the universe, and even if it does exist, it may not be reachable.
There are a number of interesting questions that we can ask:
You start at cell (1,1). Since you are restricted to only moving to orthogonally adjacent cells, you can only move up, down, left, and right. Hence, from cell (1,1) you can only move to (1,0), (1,2), (2,1), or (0,1). Even though cell (0,0) and (2,2) are touching (1,1), you cannot move directly to these cells from (1,1) since they are not orthogonally adjacent to (1,1). Cells (1,0) and (0,1) contain bombs, so you don't want to move to either of these. This means that the escape cell at (0,0) is not reachable.
Lets try answering each of the questions asked at the end of the previous section:
Here's another example of a two dimensional universe:
Observe that cells can be empty. Because of the rule that says you cannot move off the edge of the universe and that the universe does not "wrap around", you cannot move directly from (0,0) to (0,2) by moving "down". Moving up to (0,1) is not an option since there is a bomb there.
You are going to write a class called Universe
and implement four methods, each of which answers one the questions posed above.
Universe
has three attributes: n, dimensions, universe
. These attributes are intialized when an instance is created. The meaning of these attributes is as follows:
n
This is a positive integer representing the number of dimensions in the universe.dimensions
This is an n-element tuple specifying how the universe is divided into a grid of cells. Each element of the tuple is a positive integer. If the tuple is (k1, k2,...,kn), then the universe is divided
into a grid of k1 x k2 x ... x kn cells. There are k1 cells along the first dimension, k2 cells along the second dimension, and so forth. universe
This argument is an "n-dimensional list" and defines the contents of each cell in the universe. By "n-dimensional list" we mean a list that has been nested multiple times, where the number of "nestings" depends on n, the number of dimensions in the universe. In a one dimensional universe, this argument is just a simple list. In a two dimensional universe, this argument is a list of lists. In a 3 dimensional universe, this argument is a list of list of lists, and so forth. You can determine the contents of any cell by addressing elements in universe
with the coordinates of the cell's position. For example, in a three dimensional universe, the contents of the cell at (0,3,5) is universe[0][3][5]
. The contents of a given cell will be one of the following predefined constants:
def does_escape_exist(self, start, gold, silver)
def find_escape_path(self, start, gold, silver)
def max_gold_obtainable(self, start, gold, silver)
def min_bars_required(self, start)
start
argument is an n-element tuple, and the gold
and silver
arguments are non-negative integers.
The method does_escape_exist(self, start, gold, silver)
returns boolean True if you can escape the universe starting from the start
cell, having gold
gold bars and silver
silver bars in your possession initially. The method returns False otherwise.
The method find_escape_path(self, start, gold, silver)
returns a path of escape in the universe, assuming that you start from the start
cell, and have gold
gold bars and silver
silver bars in your possession initially. The path returned by this method is represented by a list, and each element in the list is an n-tuple representing a cell position. The order of elements in the list corresponds to the order in which you visit cells as you move through the universe. The first element in the list should be the position of the start
cell, and the last element should be the position of an escape cell. If no path of escape exists, this method returns None
.
The method max_gold_obtainable(self, start, gold, silver)
returns an integer representing the maximum number of gold bars with which you can escape starting from the start
cell, having gold
gold bars and silver
silver bars in your possession initially. If it is not possible to escape, then return -1.
The method min_bars_required(self, start)
returns an
integer representing the minimum total number of bars (silver + gold) that you need to have initially so that you can escape the universe starting from the start
cell. If it is not possible to escape no matter how many bars you start with, then this method returns -1.
To get you started with testing, and to provide you some examples of how we expect the methods to work, we're providing you with testuniverse.py.
We recognize that writing a set of test cases, complete with docstrings, can be very time consuming. As such, we're only asking that you submit test cases for the does_escape_exist
method. The set of tests that you write should be sufficient to convince yourself (and us) that your code is correct.
Note: even though you don't have to submit your tests for the other methods, you should still test them!
does_escape_exist
and find_escape_path
will be worth roughly 50% of your grade, a solution for max_gold_obtainable
will be worth roughly 30%, and a solution for min_bars_required
will be worth roughly 20%.
does_escape_exist
and find_escape_path
without worrying about the other two methods. If you can implement a solution for these two methods, then a solution for max_gold_obtainable
and min_bars_required
should only involve a moderate amount of additional work. universe.py
Implement the four methods described earlier. Make sure you add docstrings to these methods, as they are not provided in the starter code.
testuniverse.py
This should include your test cases for the does_path_exist
method. You don't have to submit your tests for the other methods, but still be sure to test them!