# 61A Homework 7

Due by 11:59pm on Tuesday, 4/8

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

Readings. Section 2.7 and 2.8 of Composing Programs.

Introduction. This assignment is officially due in three weeks, because of the upcoming test and spring break. However, you may find some of the material useful in preparing for the test, so we suggest that you complete a week ahead of time. We will run the autograder accordingly.

Q1. Write a class Amount that represents a collection of nickels and pennies. Include a property method called value that computes the total monetary value of the amount from the nickels and pennies.

Do not add an instance attribute called value to each Amount instance. Instead, an Amount should have only two instance attributes: nickels and pennies. You do not need to support direct assignment to value. (You are welcome to add that feature as well; see the relevant Python Property docs).

Finally, write a subclass MinimalAmount with base class Amount that overrides the constructor so that all amounts are minimal upon construction. An Amount instance is minimal if it has no more than four pennies, but the same value as the nickel and penny quantities passed as arguments:

```class Amount(object):
"""An amount of nickels and pennies.

>>> a = Amount(3, 7)
>>> a.nickels
3
>>> a.pennies
7
>>> a.value
22
>>> a.nickels = 5
>>> a.value
32
"""

class MinimalAmount(Amount):
"""An amount of nickels and pennies that is initialized with no more than
four pennies, by converting excess pennies to nickels.

>>> a = MinimalAmount(3, 7)
>>> a.nickels
4
>>> a.pennies
2
>>> a.value
22
"""
```

Q2. Section 2.8.4 of the online textbook and slide 20 of Lecture 17 describe one way to get data-directed programming, in which the data "decide" how any particular method is to be handled.

Write a data-directed apply function that computes the area or perimeter of either a square or a rectangle. As suggested in the readings, use a dictionary to store the implementations of each function for each type:

```class Square(object):
def __init__(self, side):
self.side = side

class Rect(object):
def __init__(self, width, height):
self.width = width
self.height = height

def apply(operator_name, shape):
"""Apply operator to shape.

>>> apply('area', Square(10))
100
>>> apply('perimeter', Square(5))
20
>>> apply('area', Rect(5, 10))
50
>>> apply('perimeter', Rect(2, 4))
12
"""
```

Q3. In lecture, we talked about using binary search trees to implement sets of values, but one could also use various kinds of sequence to do the job, such as the rlist type from earlier in the semester, here recast as a class:

```class Rlist:
"""A recursive list consisting of a first element and the rest.

>>> s = Rlist(1, Rlist(2, Rlist(3)))
>>> s.rest
Rlist(2, Rlist(3))
>>> len(s)
3
>>> s[1]
2
"""

class EmptyList:
def __len__(self):
return 0
def __repr__(self):
return "Rlist.empty"

empty = EmptyList()

def __init__(self, first, rest=empty):
assert type(rest) is Rlist or rest is Rlist.empty
self.first = first
self.rest = rest

def __getitem__(self, index):
if index == 0:
return self.first
else:
if self.rest is Rlist.empty:
raise IndexError("Rlist index out of bounds")
return self.rest[index-1]

def __len__(self):
return 1 + len(self.rest)

def __repr__(self):
"""Return a string that would evaluate to s."""
if self.rest is Rlist.empty:
rest = ''
else:
rest = ', ' + repr(self.rest)
return 'Rlist({0}{1})'.format(self.first, rest)
```

Complete the following partial implementation of sets represented as sorted recursive lists by filling in definitions for rlist_adjoin, drlist_adjoin, rlist_intersect and rlist_union. All of these functions should take Θ(n) time, where n is the size of the input list(s):

```class rlist_set:
"""A set of arbitrary items that have an ordering (i.e., that
define <=, <, and other relational operations between them)."""

# Representation: rlist_sets are represented by Rlists of items
# maintained in sorted order.

def __init__(self, *initial_items):
"""A set that initially contains the values in
INITIAL_ITEMS, which can be any iterable."""
self._s = Rlist.empty
for v in initial_items:

@staticmethod
def _make_set(r):
"""An internal method for creating rlist_sets out of rlists."""
result = rlist_set()
result._s = r
return result

def as_rlist(self):
"""An Rlist of my values in sorted order.  This Rlist must not be modified."""
return self._s

def empty(self):
return self._s is Rlist.empty

def __contains__(self, v):
"""True iff I currently contain the value V.
>>> s = rlist_set(3, 8, 7, 1)
>>> s.__contains__(7)
True
>>> 7 in s     # __contains__ defines 'in'
True
>>> 9 in s
False"""
if self.empty() or self._s.first > v:
return False
elif self._s.first == v:
return True
else:
return v in self._s.rest

def __repr__(self):
result = "{"
s = self._s
while s is not Rlist.empty:
if result != "{":
result += ", "
result += repr(s.first)
s = s.rest
return result + "}"

def intersection(self, other_set):
"""Return a set containing all elements common to rlist_sets
SELF and OTHER_SET.

>>> s = rlist_set(1, 2, 3)
>>> t = rlist_set(2, 3, 4)
>>> s.intersection(t)
{2, 3}
"""
return rlist_set._make_set(rlist_intersect(self._s, other_set._s))

"""Return a set containing all elements of s and element v.
>>> s = rlist_set(1, 2, 3)
{1, 2, 2.5, 3}
{0.5, 1, 2, 3}
{1, 2, 3}
"""

"""Destructively changes me to the result of adjoining V, returning the modified
set."""

def union(self, other_set):
"""Return a set containing all elements either in myself or OTHER_SET.

>>> s0 = rlist_set(2, 3)
>>> t0 = rlist_set(3, 5)
>>> s.union(t)
{1, 2, 3, 5}
>>> s0.union(t)
{1, 2, 3, 5}
>>> rlist_set().union(s0.intersection(t))
{3}
"""
return rlist_set._make_set(rlist_union(self._s, other_set._s))

"""Assuming S is an Rlist in sorted order, a new Rlist that contains all the original
values, plus V (if not already present) in sorted order."""

"""Destructively add V to the appropriate place in sorted Rlist S, if it is not already
present, returning the modified Rlist."""

def rlist_intersect(s1, s2):
"""Assuming S1 and S2 are two Rlists in sorted order, return a new Rlist in
sorted order containing exactly the values both have in common, in sorted order."""

def rlist_union(s1, s2):
"""Assuming S1 and S2 are two Rlists in sorted order, return a new Rlist in
sorted order containing the union of the values in both, in sorted order."""
```

Q4. Complete the following, somewhat more direct version of the BinTree type from lecture by filling in the inorder_tree_iter class at the end:

```class BinTree:
"""A binary tree."""

def __init__(self, label, left=None, right=None):
"""The binary tree node with given LABEL, whose left
and right children are BinTrees LEFT and RIGHT, which
default to the empty tree."""
self._label = label
self._left = left or BinTree.empty
self._right = right or BinTree.empty

@property
def is_empty(self):
"""This tree contains no labels or children."""
return self is BinTree.empty

@property
def label(self):
return self._label

@property
def left(self):
return self._left

@property
def right(self):
return self._right

def set_left(self, newval):
"""Assuming NEWVAL is a BinTree, sets SELF.left to NEWVAL."""
assert isinstance(newval, BinTree)
self._left = newval

def set_right(self, newval):
"""Assuming NEWVAL is a BinTree, sets SELF.right to NEWVAL."""
assert isinstance(newval, BinTree)
self._right = newval

def inorder_values(self):
"""An iterator over my labels in inorder (left tree labels, recursively,
then my label, then right tree labels).
>>> T = BinTree(10, BinTree(5, BinTree(2), BinTree(6)), BinTree(15))
>>> for v in T.inorder_values():
...     print(v, end=" ")
2 5 6 10 15 """
return inorder_tree_iter(self)

# A placeholder, initialized right after the class.
empty = None

def __repr__(self):
if self.is_empty:
return "BinTree.empty"
else:
args = repr(self.label)
if not self.left.is_empty or not self.right.is_empty:
args += ', {0}, {1}'.format(repr(self.left), repr(self.right))
return 'BinTree({0})'.format(args)

class EmptyBinTree(BinTree):
"""Represents the empty tree.  There should only be one of these."""

def __init__(self):
pass

@property
def is_empty(self): return True
@property
def label(self): raise NotImplemented
@property
def left(self): raise NotImplemented
@property
def right(self): raise NotImplemented

def set_left(self, newval): raise NotImplemented
def set_right(self, newval): raise NotImplemented

# Set the empty BinTree (we could only do this after defining EmptyBinTree
BinTree.empty = EmptyBinTree()

class inorder_tree_iter:
def __init__(self, the_tree):
self._work_queue = [ the_tree ]

def __next__(self):
while len(self._work_queue) > 0:
subtree_or_label = self._work_queue.pop()
raise StopIteration

def __iter__(self): return self
```

Notes: (1) As is common practice, we add and remove items from the end of _work_queue rather than the beginning. (2) Since BinTrees can also be EmptyBinTrees, use the built-in isinstance function to test whether something is a BinTree. See, for example, the set_left method.

Q5. Here is an implementation of sets using BinTree from the preceding problem:

```class bintree_set:
"""A set of arbitrary items that have an ordering (i.e., that
define <=, <, and other relational operations between them)."""

# Representation: bintree_sets are represented by BinTrees used as binary search trees.

def __init__(self, *initial_items):
"""A set that initially contains the values in
INITIAL_ITEMS, which can be any iterable."""
self._s = BinTree.empty
for v in initial_items:

def __repr__(self):
return "{" + ", ".join(map(repr, self._s.inorder_values())) + "}"

def __contains__(self, v):
"""True iff I currently contain the value V.
>>> s = bintree_set(3, 8, 7, 1)
>>> s.__contains__(7)
True
>>> 7 in s     # __contains__ defines 'in'
True
>>> 9 in s
False"""
s = self._s
while not s is BinTree.empty and s._label != v:
if v < s._label:
s = s.left
else:
s = s.right
return not s.is_empty

@staticmethod
def _make_set(b):
"""An internal method for creating a bintree_set out of bintree B."""
result = bintree_set()
result._s = b
return result

"""Return a set containing all elements of s and element v."""
if T.is_empty:
return BinTree(x)
elif x == T.label:
return T
elif x < T.label:
else:

"""Destructively adjoin V to my values, returning modified set."""
if T.is_empty:
return BinTree(x)
elif x == T.label:
return T
elif x < T.label:
return T
else:
return T
return self

def intersection(self, other_set):
"""Return a set containing all elements common to bintree_sets
SELF and OTHER_SET.

>>> s = bintree_set(1, 2, 3)
>>> t = bintree_set(2, 3, 4)
>>> s.intersection(t)
{2, 3}
"""
list1 = rlist_set(*self._s.inorder_values()).as_rlist()
list2 = rlist_set(*other_set._s.inorder_values()).as_rlist()
return bintree_set._make_set(ordered_sequence_to_tree(rlist_intersect(list1, list2)))

def union(self, other_set):
"""Return a set containing all elements either in myself or OTHER_SET.

>>> s0 = bintree_set(2, 3)
>>> t0 = bintree_set(3, 5)
>>> s.union(t)
{1, 2, 3, 5}
>>> s0.union(t)
{1, 2, 3, 5}
>>> bintree_set().union(s0.intersection(t))
{3}"""
list1 = rlist_set(*self._s.inorder_values()).as_rlist()
list2 = rlist_set(*other_set._s.inorder_values()).as_rlist()
return bintree_set._make_set(ordered_sequence_to_tree(rlist_union(list1, list2)))
```

Unlike the simple intersection done in lecture, we'd like the results of union and intersection to be "bushy". In order to complete the coercion from an ordered sequence set to a tree set, implement partial_sequence_to_tree below. If you can, implement the function to run in Θ(n) time in the length of the input list.

Hint: This function requires two recursive calls. The first call builds a left child out of the first left_size elements of s; Then, the next element is used as the label of the returned tree. Finally, the second recursive call builds the right child out of the next right_size elements. In total, (left_size + 1 + right_size) = n, where 1 is for the label:

```def partial_sequence_to_tree(s, n):
"""Return a tuple (b, r), where b is a tree of the first N elements of Rlist S, and
r is the Rlist of the remaining elements of S. A tree is balanced if

(a) the number of entries in its left branch differs from the number
of entries in its right branch by at most 1, and

(b) its non-empty branches are also balanced trees.

Examples of balanced trees:

Tree(1)                    # branch difference 0 - 0 = 0
Tree(1, Tree(2), None)     # branch difference 1 - 0 = 1
Tree(1, None, Tree(2))     # branch difference 0 - 1 = -1
Tree(1, Tree(2), Tree(3))  # branch difference 1 - 1 = 0

Examples of unbalanced trees:

BinTree(1, BinTree(2, BinTree(3)), None)  # branch difference 2 - 0 = 2
BinTree(1, BinTree(2, BinTree(3), None),
BinTree(4, BinTree(5, BinTree(6), None), None)) # Unbalanced right branch

>>> s = Rlist(1, Rlist(2, Rlist(3, Rlist(4, Rlist(5)))))
>>> partial_sequence_to_tree(s, 3)
(BinTree(2, BinTree(1), BinTree(3)), Rlist(4, Rlist(5)))
>>> t = Rlist(-2, Rlist(-1, Rlist(0, s)))
>>> partial_sequence_to_tree(t, 7)[0]
BinTree(1, BinTree(-1, BinTree(-2), BinTree(0)), BinTree(3, BinTree(2), BinTree(4)))
>>> partial_sequence_to_tree(t, 7)[1]
Rlist(5)
"""
if n == 0:
return BinTree.empty, s
left_size = (n-1)//2
right_size = n - left_size - 1

def ordered_sequence_to_tree(s):
"""Return a balanced tree containing the elements of ordered Rlist s.

Note: this implementation is complete, but the definition of partial_sequence_to_tree
above is not complete.

>>> ordered_sequence_to_tree(Rlist(1, Rlist(2, Rlist(3))))
BinTree(2, BinTree(1), BinTree(3))
>>> b = rlist_set(*range(1, 20, 3))
>>> elements = b.as_rlist()
>>> elements
Rlist(1, Rlist(4, Rlist(7, Rlist(10, Rlist(13, Rlist(16, Rlist(19)))))))
>>> ordered_sequence_to_tree(elements)
BinTree(10, BinTree(4, BinTree(1), BinTree(7)), BinTree(16, BinTree(13), BinTree(19)))
"""
return partial_sequence_to_tree(s, len(s))[0]
```

Q6. Mario needs to jump over a sequence of Piranha plants, represented as a string of spaces (no plant) and P's (plant!). He only moves forward, and he can either step (move forward one space) or jump (move forward two spaces) from each position. How many different ways can Mario traverse a level without stepping or jumping into a Piranha plant? Assume that every level begins with a space (where Mario starts) and ends with a space (where Mario must end up):

```def mario_number(level):
"""Return the number of ways that Mario can perform a sequence of steps
or jumps to reach the end of the level without ever landing in a Piranha
plant. Assume that every level begins and ends with a space.

>>> mario_number(' P P ')   # jump, jump
1
>>> mario_number(' P P  ')   # jump, jump, step
1
>>> mario_number('  P P ')  # step, jump, jump
1
>>> mario_number('   P P ') # step, step, jump, jump or jump, jump, jump
2
>>> mario_number(' P PP ')  # Mario cannot jump two plants
0
>>> mario_number('    ')    # step, jump ; jump, step ; step, step, step
3
>>> mario_number('    P    ')
9
>>> mario_number('   P    P P   P  P P    P     P ')
180
"""
```

Q7. (Extra for experts) The Rlist class can represent lists with cycles. That is, a list may contain itself as a sublist.

```>>> s = Rlist(1, Rlist(2, Rlist(3)))
>>> s.rest.rest.rest = s
>>> s[20]
3
```
This question has two parts:
1. Write a function has_cycle that returns True if and only if its argument, an Rlist instance, contains a cycle.
2. Write a function has_cycle_constant that has the same behavior as has_cycle but requires only a constant amount of space.

Hint: The solution to B is short (less than 20 lines of code), but requires a clever idea. Try to discover the solution yourself before asking around:

```def has_cycle(s):
"""Return whether Rlist s contains a cycle.

>>> s = Rlist(1, Rlist(2, Rlist(3)))
>>> s.rest.rest.rest = s
>>> has_cycle(s)
True
>>> t = Rlist(1, Rlist(2, Rlist(3)))
>>> has_cycle(t)
False
"""

def has_cycle_constant(s):
"""Return whether Rlist s contains a cycle.

>>> s = Rlist(1, Rlist(2, Rlist(3)))
>>> s.rest.rest.rest = s
>>> has_cycle_constant(s)
True
>>> t = Rlist(1, Rlist(2, Rlist(3)))
>>> has_cycle_constant(t)
False
"""