Homework 4 Solutions

Solution Files

You can find solutions for all questions in hw04.py.

The construct_check module is used in this assignment, which defines a function check. For example, a call such as

check("foo.py", "func1", ["While", "For", "Recursion"])

checks that the function func1 in file foo.py does not contain any while or for constructs, and is not an overtly recursive function (i.e., one in which a function contains a call to itself by name.)

Required questions

Q1: Taxicab Distance

An intersection in midtown Manhattan can be identified by an avenue and a street, which are both indexed by positive integers. The Manhattan distance or taxicab distance between two intersections is the number of blocks that must be traversed to reach one from the other, ignoring one-way street restrictions and construction. For example, Times Square is on 46th Street and 7th Avenue. Ess-a-Bagel is on 51st Street and 3rd Avenue. The taxicab distance between them is 9 blocks (5 blocks from 46th to 51st street and 4 blocks from 7th avenue to 3rd avenue). Taxicabs cannot cut diagonally through buildings to reach their destination!

Implement taxicab, which computes the taxicab distance between two intersections using the following data abstraction. Hint: You don't need to know what a Cantor pairing function is; just use the abstraction.

def intersection(st, ave):
    """Represent an intersection using the Cantor pairing function."""
    return (st+ave)*(st+ave+1)//2 + ave

def street(inter):
    return w(inter) - avenue(inter)

def avenue(inter):
    return inter - (w(inter) ** 2 + w(inter)) // 2

w = lambda z: int(((8*z+1)**0.5-1)/2)

def taxicab(a, b):
    """Return the taxicab distance between two intersections.

    >>> times_square = intersection(46, 7)
    >>> ess_a_bagel = intersection(51, 3)
    >>> taxicab(times_square, ess_a_bagel)
    9
    >>> taxicab(ess_a_bagel, times_square)
    9
    """
return abs(street(a)-street(b)) + abs(avenue(a)-avenue(b))

Use Ok to test your code:

python3 ok -q taxicab

The main focus of this problem is to get familiar with using data abstraction. With some previous problems involving abstract data types, it might have been possible to break the abstraction barrier and still solve the problem. This time around, the abstraction uses the Cantor pairing function to obfuscate the original data!

Through the power of abstraction however, you don't need to understand how the Cantor pairing function works. In truth, we could have also not told you anything about how the abstract data type was implemented. As long as you use the provided selectors, you should be able to solve the problem.

Speaking of which, the selectors give the street and avenue of an intersection. If we have the street and the avenue for each intersection, the taxicab distance is just the sum of the absolute difference of the two.

For more information, Wikipedia has a useful visualization.

Video walkthrough: https://youtu.be/QueVasKQQBI

Q2: Squares only

Implement the function squares, which takes in a list of positive integers. It returns a list that contains the square roots of the elements of the original list that are perfect squares. Try using a list comprehension.

You may find the round function useful.

>>> round(10.5)
10
>>> round(10.51)
11
def squares(s):
    """Returns a new list containing square roots of the elements of the
    original list that are perfect squares.

    >>> seq = [8, 49, 8, 9, 2, 1, 100, 102]
    >>> squares(seq)
    [7, 3, 1, 10]
    >>> seq = [500, 30]
    >>> squares(seq)
    []
    """
return [round(n ** 0.5) for n in s if n == round(n ** 0.5) ** 2]

Use Ok to test your code:

python3 ok -q squares

It might be helpful to construct a skeleton list comprehension to begin with:

[sqrt(x) for x in s if is_perfect_square(x)]

This is great, but it requires that we have an is_perfect_square function. How might we check if something is a perfect square?

  • If the square root of a number is a whole number, then it is a perfect square. For example, sqrt(61) = 7.81024... (not a perfect square) and sqrt(49) = 7 (perfect square).
  • Once we obtain the square root of the number, we just need to check if something is a whole number. The is_perfect_square function might look like:

    def is_perfect_square(x):
        return is_whole(sqrt(x))
  • One last piece of the puzzle: to check if a number is whole, we just need to see if it has a decimal or not. The way we've chosen to do it in the solution is to compare the original number to the round version (thus removing all decimals), but a technique employing floor division (//) or something else entirely could work too.

We've written all these helper functions to solve this problem, but they are actually all very short. Therefore, we can just copy the body of each into the original list comprehension, arriving at the solution we finally present.

Video walkthrough: https://youtu.be/YwLFB9paET0

Q3: G function

A mathematical function G on positive integers is defined by two cases:

G(n) = n,                                       if n <= 3
G(n) = G(n - 1) + 2 * G(n - 2) + 3 * G(n - 3),  if n > 3

Write a recursive function g that computes G(n). Then, write an iterative function g_iter that also computes G(n):

def g(n):
    """Return the value of G(n), computed recursively.

    >>> g(1)
    1
    >>> g(2)
    2
    >>> g(3)
    3
    >>> g(4)
    10
    >>> g(5)
    22
    >>> from construct_check import check
    >>> check(HW_SOURCE_FILE, 'g', ['While', 'For'])
    True
    """
if n in (1, 2, 3): return n return g(n-1) + 2*g(n-2) + 3*g(n-3)
def g_iter(n): """Return the value of G(n), computed iteratively. >>> g_iter(1) 1 >>> g_iter(2) 2 >>> g_iter(3) 3 >>> g_iter(4) 10 >>> g_iter(5) 22 >>> from construct_check import check >>> check(HW_SOURCE_FILE, 'g_iter', ['Recursion']) True """
if n == 1 or n == 2 or n == 3: return n a, b, c = 1, 2, 3 while n > 3: a, b, c = b, c, c + 2*b + 3*a n = n - 1 return c

Use Ok to test your code:

python3 ok -q g
python3 ok -q g_iter

This is an example of how a function might be easier to write recursively versus iteratively. Since we have defined the g function in terms of older versions of itself, the solution tends very naturally towards recursion.

The iterative solution is trickier, since we can only track a finite amount of state at a given time. We need to pick our variables carefully so that we have just the information we need to calculate the next step. In a sense, this problem is very similar to the Fibonacci sequence (assuming we start at the 0th Fibonacci number):

f(n) = n,               if n <= 2
f(n) = f(n-1) + f(n-2), if n > 2

As you may recall, the solution for Fibonacci carried two variables around for the two previous values.

def fib_iter(n):
    prev, curr = 0, 1
    while n > 0:
        prev, curr = curr, prev + curr
    return prev

Since the g function depends on the three previous values, it might make sense that we might have to track three values instead!

Consider the three previous values old, older, and oldest. To do an update, the older value ages and becomes oldest, the old value ages and becomes older. Finally, old gets the new value which is derived from the three previous values: old + 2 * older + 3 * oldest.

Video walkthrough: https://youtu.be/pltx7u2kGGE

Q4: Ping pong

The ping-pong sequence counts up starting from 1 and is always either counting up or counting down. At element k, the direction switches if k is a multiple of 7 or contains the digit 7. The first 30 elements of the ping-pong sequence are listed below, with direction swaps marked using brackets at the 7th, 14th, 17th, 21st, 27th, and 28th elements:

1 2 3 4 5 6 [7] 6 5 4 3 2 1 [0] 1 2 [3] 2 1 0 [-1] 0 1 2 3 4 [5] [4] 5 6

Implement a function pingpong that returns the nth element of the ping-pong sequence. Do not use any assignment statements; however, you may use def statements.

Hint: If you're stuck, try implementing pingpong first using assignment and a while statement. Any name that changes value will become an argument to a function in the recursive definition.

def pingpong(n):
    """Return the nth element of the ping-pong sequence.

    >>> pingpong(7)
    7
    >>> pingpong(8)
    6
    >>> pingpong(15)
    1
    >>> pingpong(21)
    -1
    >>> pingpong(22)
    0
    >>> pingpong(30)
    6
    >>> pingpong(68)
    2
    >>> pingpong(69)
    1
    >>> pingpong(70)
    0
    >>> pingpong(71)
    1
    >>> pingpong(72)
    0
    >>> pingpong(100)
    2
    >>> from construct_check import check
    >>> check(HW_SOURCE_FILE, 'pingpong', ['Assign', 'AugAssign'])
    True
    """
def pingpong_next(x, i, step): if i == n: return x return pingpong_next(x + step, i + 1, next_dir(step, i+1)) def next_dir(step, i): if i % 7 == 0 or has_seven(i): return -step return step return pingpong_next(1, 1, 1) # Alternate solution def pingpong(n): if n <= 7: return n return direction(n) + pingpong(n-1) def direction(n): if n < 7: return 1 if (n-1) % 7 == 0 or has_seven(n-1): return -1 * direction(n-1) return direction(n-1)

You may use the function has_seven, which returns True if a number k contains the digit 7 at least once.

Use Ok to test your code:

python3 ok -q pingpong

This is a fairly involved recursion problem, which we will first solve through iteration and then convert to a recursive solution.

Note that at any given point in the sequence, we need to keep track of the current value of the sequence (this is the value that might be output) as well as the current index of the sequence (how many items we have seen so far, not actually output).

For example, 14th element has value 0, but it's the 14th index in the sequence. We will refer to the value as x and the index as i. An iterative solution may look something like this:

def pingpong(n):
    i = 1
    x = 1
    while i < n:
        x += 1
        i += 1
    return x

Hopefully, it is clear to you that this has a big problem. This doesn't account for changes in directions at all! It will work for the first seven values of the sequence, but then fail after that. To fix this, we can add in a check for direction, and then also keep track of the current direction to make our lives a bit easier (it's possible to compute the direction from scratch at each step, see the direction function in the alternate solution).

def pingpong(n):
    i = 1
    x = 1
    is_up = True
    while i < n:
        is_up = next_dir(...)
        if is_up:
            x += 1
        else:
            x -= 1
        i += 1
    return x

All that's left to do is to write the next_dir function, which will take in the current direction and index and then tell us what direction to go in next (which could be the same direction):

def next_dir(is_up, i):
    if i % 7 == 0 or has_seven(i):
        return not is_up
    return is_up

There's a tiny optimization we can make here. Instead of calculating an increment based on the value of is_up, we can make it directly store the direction of change into the variable (next_dir is also updated, see the solution for the new version):

def pingpong(n):
    i = 1
    x = 1
    step = 1
    while i < n:
        step = next_dir(step, i)
        x += step
        i += 1
    return x

This will work, but it uses assignment. To convert it to an equivalent recursive version without assignment, make each local variable into a parameter of a new helper function, and then add an appropriate base case. Lastly, we seed the helper function with appropriate starting values by calling it with the values we had in the iterative version.

You should be able to convince yourself that the version of pingpong in the solutions has the same logic as the iterative version of pingpong above.

Video walkthrough: https://youtu.be/74gwPjgrN_k

def has_seven(k):
    """Returns True if at least one of the digits of k is a 7, False otherwise.

    >>> has_seven(3)
    False
    >>> has_seven(7)
    True
    >>> has_seven(2734)
    True
    >>> has_seven(2634)
    False
    >>> has_seven(734)
    True
    >>> has_seven(7777)
    True
    """
    if k % 10 == 7:
        return True
    elif k < 10:
        return False
    else:
        return has_seven(k // 10)

Q5: Count change

Once the machines take over, the denomination of every coin will be a power of two: 1-cent, 2-cent, 4-cent, 8-cent, 16-cent, etc. There will be no limit to how much a coin can be worth.

A set of coins makes change for n if the sum of the values of the coins is n. For example, the following sets make change for 7:

  • 7 1-cent coins
  • 5 1-cent, 1 2-cent coins
  • 3 1-cent, 2 2-cent coins
  • 3 1-cent, 1 4-cent coins
  • 1 1-cent, 3 2-cent coins
  • 1 1-cent, 1 2-cent, 1 4-cent coins

Thus, there are 6 ways to make change for 7. Write a function count_change that takes a positive integer n and returns the number of ways to make change for n using these coins of the future:

def count_change(amount):
    """Return the number of ways to make change for amount.

    >>> count_change(7)
    6
    >>> count_change(10)
    14
    >>> count_change(20)
    60
    >>> count_change(100)
    9828
    """
return count_using(1, amount) def count_using(min_coin, amount): if amount < 0: return 0 elif amount == 0: return 1 elif min_coin > amount: return 0 else: with_min = count_using(min_coin, amount - min_coin) without_min = count_using(2*min_coin, amount) return with_min + without_min

Hint: you may find it helpful to refer to the implementation of count_partitions.

Use Ok to test your code:

python3 ok -q count_change

This is remarkably similar to the count_partitions problem, with a few minor differences:

  • A maximum partition size m is not given, though we create something similar using a helper function.
  • Partition size is not linear, but rather multiples of two. To get to the next size up, we multiply by two (if you decided to start with a max coin and go down, you should divide by two. More on this below).

One other implementation detail here is that we enforce a minimum coin size, rather than a maximum partition size. Many students attempted to find a maximum coin and work their way down. That will also work, but you should understand why going from smaller to larger works as well. As long as there is some ordering on the coins being enforced, we ensure we cover all the combinations of coins without any duplicates.

Video walkthrough: https://youtu.be/EgZJPNFnoxM

Extra questions

Extra questions are not worth extra credit and are entirely optional. They are designed to challenge you to think creatively!

Q6: Anonymous factorial

The recursive factorial function can be written as a single expression by using a conditional expression.

>>> fact = lambda n: 1 if n == 1 else mul(n, fact(sub(n, 1)))
>>> fact(5)
120

However, this implementation relies on the fact (no pun intended) that fact has a name, to which we refer in the body of fact. To write a recursive function, we have always given it a name using a def or assignment statement so that we can refer to the function within its own body. In this question, your job is to define fact recursively without giving it a name!

Write an expression that computes n factorial using only call expressions, conditional expressions, and lambda expressions (no assignment or def statements). Note in particular that you are not allowed to use make_anonymous_factorial in your return expression. The sub and mul functions from the operator module are the only built-in functions required to solve this problem:

from operator import sub, mul

def make_anonymous_factorial():
    """Return the value of an expression that computes factorial.

    >>> make_anonymous_factorial()(5)
    120
    >>> from construct_check import check
    >>> check(HW_SOURCE_FILE, 'make_anonymous_factorial', ['Assign', 'AugAssign', 'FunctionDef', 'Recursion'])
    True
    """
return (lambda f: lambda k: f(f, k))(lambda f, k: k if k == 1 else mul(k, f(f, sub(k, 1))))

Use Ok to test your code:

python3 ok -q make_anonymous_factorial