Trees
Q1: Replace Leaf
Define replace_leaf
, which takes a tree t
, a value old
, and a value new
. replace_leaf
returns a new tree that’s the same as t
except that every leaf value equal to old
has been replaced with new
.
1 | def replace_leaf(t, old, new): |
Mobiles
Acknowledgements. This mobile example is based on a classic problem from Structure and Interpretation of Computer Programs, Section 2.2.2.
A mobile is a type of hanging sculpture. A binary mobile consists of two sides. Each side is a rod of a certain length, from which hangs either a weight or another mobile.
We will represent a binary mobile using the data abstractions below.
- A
mobile
has a leftside
and a rightside
. - A
side
has a positive length and something hanging at the end, either amobile
orweight
. - A
weight
has a positive size.Q2: Weights
Implement theweight
data abstraction by completing theweight
constructor and thesize
selector so that a weight is represented using a two-element list where the first element is the string'weight'
. Thetotal_weight
example is provided to demonstrate use of the mobile, side, and weight abstractions.
1 | def mobile(left, right): |
Q3: Balanced
Implement the balanced
function, which returns whether m
is a balanced mobile. A mobile is balanced if two conditions are met:
- The torque(转矩) applied by its left side is equal to that applied by its right side. Torque of the left side is the length of the left rod multiplied by the total weight hanging from that rod. Likewise for the right.
- Each of the mobiles hanging at the end of its sides is balanced.
Hint: You may find it helpful to assume that weights themselves are balanced.
1 | def balanced(m): |
The balanced weights assumption is important, since we will be solving this recursively like many other tree problems (even though this is not explicitly a tree).
Base case: if we are checking a weight, then we know that this is balanced. Why is this an appropriate base case? There are two possible approaches to this:
1. Because we know that our data structures so far are trees, weights are the simplest possible tree since we have chosen to implement them as leaves.
2. We also know that from an ADT standpoint, weights are the terminal item in a mobile. There can be no further mobile structures under this weight, so it makes sense to stop check here.
Otherwise: note that it is important to do a recursive call to check if both sides are balanced. However, we also need to do the basic comparison of looking at the total weight of both sides as well as their length. For example if both sides are a weight, trivially, they will both be balanced. However, the torque must be equal in order for the entire mobile to balanced (i.e. it’s insufficient to just check if the sides are balanced).
Q4: Totals
Implement totals_tree
, which takes a mobile
(or weight
) and returns a tree
whose root is its total weight and whose branches are trees for the ends of the sides.
My solution
1 | def totals_tree(m): |
The PDF solution, which I think is so intellectual!!
1 | def totals_tree(m): |
Mutable functions
Q5: Counter
Define a function make_counter
that returns a counter
function, which takes a string and returns the number of times that the function has been called on that string.
My solution(a little stupid, not general)
1 | def make_counter(): |
The PDF solution(smart and beautiful, general)
1 | def make_counter(): |
1 | def make_counter(): |
Q6: Next Fibonacci
Write a function make_fib
that returns a function that returns the next Fibonacci number each time it is called. (The Fibonacci sequence begins with 0 and then 1, after which each element is the sum of the preceding two.) Use a nonlocal
statement!
1 | def make_fib(): |
Q7: Password Protected Account
In lecture, we saw how to use functions to create mutable objects. Here, for example, is the function make_withdraw
which produces a function that can withdraw money from an account:
1 | def make_withdraw(balance): |
Write a version of the make_withdraw
function that returns password-protected withdraw functions. That is, make_withdraw
should take a password argument (a string) in addition to an initial balance. The returned function should take two arguments: an amount to withdraw and a password.
A password-protected withdraw
function should only process withdrawals that include a password that matches the original. Upon receiving an incorrect password, the function should:
Store that incorrect password in a list, and
Return the string ‘Incorrect password’.
If a withdraw function has been called three times with incorrect passwordsp1
,p2
, andp3
, then it is locked. All subsequent calls to the function should return:1
"Your account is locked. Attempts: [<p1>, <p2>, <p3>]"
The incorrect passwords may be the same or different:
My solution1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44def make_withdraw(balance, password):
"""Return a password-protected withdraw function.
>>> w = make_withdraw(100, 'hax0r')
>>> w(25, 'hax0r')
75
>>> error = w(90, 'hax0r')
>>> error
'Insufficient funds'
>>> error = w(25, 'hwat')
>>> error
'Incorrect password'
>>> new_bal = w(25, 'hax0r')
>>> new_bal
50
>>> w(75, 'a')
'Incorrect password'
>>> w(10, 'hax0r')
40
>>> w(20, 'n00b')
'Incorrect password'
>>> w(10, 'hax0r')
"Your account is locked. Attempts: ['hwat', 'a', 'n00b']"
>>> w(10, 'l33t')
"Your account is locked. Attempts: ['hwat', 'a', 'n00b']"
>>> type(w(10, 'l33t')) == str
True
"""
"*** YOUR CODE HERE ***"
l = []
num = 0
def withdraw(amount, p):
nonlocal num, balance
if num == 3:
return 'Your account is locked. Attempts: ' + str(l)
if p != password:
l.append(p)
num += 1
return 'Incorrect password'
if amount > balance:
return 'Insufficient funds'
balance -= amount
return balance
return withdrawThe PDF solution, which have not a num variable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41def make_withdraw(balance, password):
"""Return a password-protected withdraw function.
>>> w = make_withdraw(100, 'hax0r')
>>> w(25, 'hax0r')
75
>>> error = w(90, 'hax0r')
>>> error
'Insufficient funds'
>>> error = w(25, 'hwat')
>>> error
'Incorrect password'
>>> new_bal = w(25, 'hax0r')
>>> new_bal
50
>>> w(75, 'a')
'Incorrect password'
>>> w(10, 'hax0r')
40
>>> w(20, 'n00b')
'Incorrect password'
>>> w(10, 'hax0r')
"Your account is locked. Attempts: ['hwat', 'a', 'n00b']"
>>> w(10, 'l33t')
"Your account is locked. Attempts: ['hwat', 'a', 'n00b']"
>>> type(w(10, 'l33t')) == str
True
"""
attempts = []
def withdraw(amount, password_attempt):
nonlocal balance
if len(attempts) == 3:
return 'Your account is locked. Attempts: ' + str(attempts)
if password_attempt != password:
attempts.append(password_attempt)
return 'Incorrect password'
if amount > balance:
return 'Insufficient funds'
balance = balance - amount
return balance
return withdrawQ8: Joint Account
Suppose that our banking system requires the ability to make joint accounts. Define a function
make_joint
that takes three arguments.A password-protected
withdraw
function,The password with which that
withdraw
function was defined, andA new password that can also access the original account.
Themake_joint
function returns awithdraw
function that provides additional access to the original account using either the new or old password. Both functions draw from the same balance. Incorrect passwords provided to either function will be stored and cause the functions to be locked after three wrong attempts.
Hint: The solution is short (less than 10 lines) and contains no string literals! The key is to call withdraw
with the right password and amount, then interpret the result. You may assume that all failed attempts to withdraw will return some string (for incorrect passwords, locked accounts, or insufficient funds), while successful withdrawals will return a number.
Use type(value) == str
to test if some value
is a string:
1 | def make_joint(withdraw, old_password, new_password): |
Generators
Q9: Generate Paths
Define a generator function generate_paths
which takes in a tree t
, a value x
, and yields each path from the root of t
to a node that has label x
. Each path should be represented as a list of the labels along that path in the tree. You may yield the paths in any order.
1 | def generate_paths(t, x): |
If our current label is equal to x, we’ve found a path from the root to a node containing x containing only our current label, so we should yield that. From there, we’ll see if there are any paths starting from one of our branches that ends at a node containing x. If we find these “partial paths” we can simply add our current label to the beinning of a path to obtain a path starting from the root.
In order to do this, we’ll create a generator for each of the branches which yields these “partial paths”. By calling generate_paths on each of the branches, we’ll create exactly this generator! Then, since a generator is also an iterable, we can iterate over the paths in this generator and yield the result of concatenating it with our current label.
Interval
Acknowledgements. This interval arithmetic example is based on a classic problem from Structure and Interpretation of Computer Programs, Section 2.1.4.
Introduction. Alyssa P. Hacker is designing a system to help people solve engineering problems. One feature she wants to provide in her system is the ability to manipulate inexact quantities (such as measured parameters of physical devices) with known precision, so that when computations are done with such approximate quantities the results will be numbers of known precision.
Alyssa’s idea is to implement interval arithmetic as a set of arithmetic operations for combining “intervals” (objects that represent the range of possible values of an inexact quantity). The result of adding, subracting, multiplying, or dividing two intervals is itself an interval, representing the range of the result.
Alyssa postulates the existence of an abstract object called an “interval” that has two endpoints: a lower bound and an upper bound. She also presumes that, given the endpoints of an interval, she can construct the interval using the data constructor interval. Using the constructor and selectors, she defines the following operations:
1 | def str_interval(x): |
Q10: Interval Abstraction
Alyssa’s program is incomplete because she has not specified the implementation of the interval abstraction. She has implemented the constructor for you; fill in the implementation of the selectors.
1 | def interval(a, b): |
Louis Reasoner has also provided an implementation of interval multiplication. Beware: there are some data abstraction violations, so help him fix his code before someone sets it on fire.
1 | def mul_interval(x, y): |
The modification
1 | def mul_interval(x, y): |
Q11: Sub Interval
Using reasoning analogous to Alyssa’s, define a subtraction function for intervals. Try to reuse functions that have already been implemented if you find yourself repeating code.
1 | def sub_interval(x, y): |
Q12: Div Interval
Alyssa implements division below by multiplying by the reciprocal of y
. Ben Bitdiddle, an expert systems programmer, looks over Alyssa’s shoulder and comments that it is not clear what it means to divide by an interval that spans zero. Add an assert
statement to Alyssa’s code to ensure that no such interval is used as a divisor:
1 | def div_interval(x, y): |
Q13: Par Diff
After considerable work, Alyssa P. Hacker delivers her finished system. Several years later, after she has forgotten all about it, she gets a frenzied call from an irate user, Lem E. Tweakit. It seems that Lem has noticed that the formula for parallel resistors can be written in two algebraically equivalent ways:
1 | par1(r1, r2) = (r1 * r2) / (r1 + r2) |
or
1 | par2(r1, r2) = 1 / (1/r1 + 1/r2) |
He has written the following two programs, each of which computes the parallel_resistors
formula differently::
1 | def par1(r1, r2): |
Lem complains that Alyssa’s program gives different answers for the two ways of computing. This is a serious complaint.
Demonstrate that Lem is right. Investigate the behavior of the system on a variety of arithmetic expressions. Make some intervals r1
and r2
, and show that par1
and par2
can give different results.
1 | def check_par(): |
1 | def check_par(): |
Video walkthrough: https://youtu.be/H8slb5KCbU4
Q14: Multiple References
Eva Lu Ator, another user, has also noticed the different intervals computed by different but algebraically equivalent expressions. She says that the problem is multiple references to the same interval.
The Multiple References Problem: a formula to compute with intervals using Alyssa’s system will produce tighter error bounds if it can be written in such a form that no variable that represents an uncertain number is repeated.
Thus, she says, par2 is a better program for parallel resistances than par1. Is she right? Why? Write a function that returns a string containing a written explanation of your answer:
1 | def multiple_references_explanation(): |
Video walkthrough: https://youtu.be/H8slb5KCbU4
Q15: Quadratic
Write a function quadratic
that returns the interval of all values f(t) such that t is in the argument interval x and f(t) is a quadratic function:
1 | f(t) = a*t*t + b*t + c |
Make sure that your implementation returns the smallest such interval, one that does not suffer from the multiple references problem.
Hint: the derivative f'(t) = 2*a*t + b
, and so the extreme point of the quadratic is -b/(2*a)
:
1 | def quadratic(x, a, b, c): |
Video walkthrough: https://youtu.be/qgSn_RNBs4A