61A Homework 9

Due by 11:59pm on Saturday, 07/26

Readings: You might find the following references useful:

Submission: See the online submission instructions. We have provided a hw9.py starter file for the questions below.

Table of Contents

Question 1

Implement the iterable interface for the Link class. Do not assume that we already have the __getitem__ method defined.

class Link:
    """A recursive list, with Python integration."""
    empty = None

    def __init__(self, first, rest=empty):
        self.first = first
        self.rest = rest

    def __repr__(self):
        if self.rest is Link.empty:
            rest = ''
        else:
            rest = ', ' + repr(self.rest)
        return 'Link({}{})'.format(self.first, rest)

    def __str__(self):
        if self.rest is Link.empty:
            rest = ''
        else:
            rest = ' ' + str(self.rest)[2:-2]
        return '< {}{} >'.format(self.first, rest)

    """
    Implement the iterable interface for the Link class.
    You will need to return an instance of an iterator 
    object in the __iter__ method.
    """
    def __iter__(self):
        """
        >>> l = list_to_link([1, 2, 3, 4])
        >>> i = iter(l)
        >>> hasattr(i, '__next__')
        True
        >>> l2 = list_to_link([3, 1, 4, 1])
        >>> for el in l2:
        ...    print(el)
        ...
        3
        1
        4
        1
        """
        "*** YOUR CODE HERE ***"

class LinkedListIterator:
    "*** YOUR CODE HERE ***"

def list_to_link(lst):
    """
    This is a convenience method which 
    converts a Python list into a linked list.
    DO NOT USE THIS IN ANY OF YOUR SOLUTIONS.
    """
    ll = Link.empty
    for elem in lst[::-1]:
        ll = Link(elem, ll)
    return ll

Recall that an iterable object must have an __iter__ method which returns an iterator object. You will need to define a LinkedListIterator class to complete the implementation.

You should be able to do a for loop over the elements of a linked list once you have correctly implemented the interface.

Question 2

Given a list of unique elements, a permutation of the list is a reordering of the elements. For example, [2, 1, 3], [1, 3, 2], and [3, 2, 1] are all permutations of the list [1, 2, 3].

Implement permutations, a function which takes in a lst and outputs a list of all permutations of lst (see doctest for examples).

def list_perms(lst):
    """
    Returns a list of all permutations of lst.
    >>> p = list_perms([1, 2, 3])
    >>> p.sort()
    >>> p
    [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
    >>> p1 = list_perms([0, 1])
    >>> p1.sort()
    >>> p1
    [[0, 1], [1, 0]]
    """
    if lst == []:
        return None # REPLACE ME
    prev_perms = list_perms(lst[1:])
    result = []
    for perm in prev_perms:
        for i in range(len(lst)):
            "*** YOUR CODE HERE ***"
    return result

Feel free to replace the for loops with list comprehensions if you would rather use that.

One application of list permutations is in solving math puzzles like this one:

___ * ___ - ___ - ___ // ___ + ___ - ___ // ___ * ___ = 7

The goal is to arrange the numbers from 1 to 9 in each of the blank spots such that the expression evaluates to 7. We have included a class MathPuzzle, which randomly generates a puzzle out of a given rng. In the example above, rng is 9. You can call print on the MathPuzzle object to get a representation like the one shown above.

The MathPuzzle class has a check_solution method, which takes in a list of numbers between 1 and the range of the puzzle and checks to see if they satisfy the expression. In this case, one solution would be

[1, 3, 2, 9, 5, 7, 4, 8, 6]

Using your code for list_perms, write solve_list_perms, which takes in a MathPuzzle object and returns a solution to the puzzle. Note that a given MathPuzzle may have multiple possible solutions; your program only needs to return the first solution you encounter.

def solve_list_perms(puzzle):
    """
    Solves a MathPuzzle using a list of permutations;
    returns the first correct answer it encounters.
    """
    "*** YOUR CODE HERE ***"

We've provided a TestPuzzle class which creates a puzzle out of a pre-determined expression so you can test your code:

class TestPuzzle(MathPuzzle):
    """
    Creates a MathPuzzle out of the given expression.
    >>> test = TestPuzzle([1, '+', 2, '*', 3])
    >>> print(test)
    ___ + ___ * ___ = 7
    >>> test.check_solution([1, 2, 3])
    True
    >>> test.check_solution([1, 3, 2])
    True
    >>> test.check_solution([2, 3, 1])
    False
    """
    def __init__(self, expression):
        self.expression = [str(el) for el in expression]
        self.puzzle = [el for el in expression if el in MathPuzzle.ops]
        self.value = eval(''.join(self.expression))
        self.range = len(self.puzzle) + 1

Be sure to make up puzzles yourself and load the file interactively. Your program may find a different solution from the one you inputted - this is fine as long as it still evaluates to the correct value.

Question 3

You might have noticed that with larger puzzles, your code for solve_list_perms was quite slow. This is because list_perms computes and outputs all permutations at once, and we have to wait for that entire process to finish before we can start testing. This implementation was also space inefficient, because it had to store all permutations when only a handful would have been a valid solution to the puzzle.

To fix these issues, write generate_perms, a more efficient way to get permutations. generate_perms is a generator function which yields each permutation of lst one by one, so it never has to store the entire set of permutations in memory at one time.

def generate_perms(lst):
    """
    Generates the permutations of lst one by one.
    >>> perms = generate_perms([1, 2, 3])
    >>> hasattr(perms, '__next__')
    True
    >>> p = list(perms)
    >>> p.sort()
    >>> p
    [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
    """
    "*** YOUR CODE HERE ***"

See Lecture 16 for an example of converting a list-based solution to a generator solution (subsets).

To see the difference, write solve_gen_perms, which solves a MathPuzzle using generate_perms instead of list_perms.

def solve_gen_perms(puzzle):
    """
    Solves a MathPuzzle by generating permutations;
    returns the first correct answer it encounters.
    """
    "*** YOUR CODE HERE ***"

Try making a larger MathPuzzle (with a rng of 15 or so) and solve it using both methods. solve_gen_perms should be significantly faster.

Question 4

Like functions, generators can also be higher-order. For this problem, we will be writing remainders_generator, which yields a series of generator objects.

def remainders_generator(m):
    """
    Takes in an integer m, and yields m different remainder groups
    of m. 

    >>> remainders_mod_four = remainders_generator(4)
    >>> for rem_group in remainders_mod_four:
    ...     for i in range(3):
    ...         next(rem_group)
    ...
    0
    4
    8
    1
    5
    9
    2
    6
    10
    3
    7
    11
    """
    "*** YOUR CODE HERE ***"

remainders_generator takes in an integer m, and yields m different generators: a generator of multiples of m, a generator of natural numbers which leave a remainder of 1 when divided by m, and so on up to a remainder of m - 1.

Note that if you have implemented this correctly, each of the generators yielded by remainder_generator will be infinite - you can keep calling next on them forever without running into a StopIteration exception.

Hint: Consider defining an inner generator function. What arguments should it take in? Where should you call it?