# Homework 3 Solutions

## Solution Files

You can find solutions for all questions in hw03.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: 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)`

:

Hint:The`fibonacci`

example in the tree recursion lecture is a good illustration of the relationship between the recursive and iterative definitions of a tree recursive problem.

```
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

### Q2: 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 *without using any assignment statements*.

You may use the function `has_seven`

, provided in the homework `.py`

file,
which returns `True`

if a number `k`

contains the digit 7 at least once.

Hint: If you're stuck, first try implementing`pingpong`

using assignment statements and a`while`

statement. Then, to convert this into a recursive solution, write a helper function that has a parameter for each variable that changes values in the body of the while loop.

```
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 helper(result, i, step):
if i == n:
return result
elif i % 7 == 0 or has_seven(i):
return helper(result - step, i + 1, -step)
else:
return helper(result + step, i + 1, step)
return helper(1, 1, 1)
# Alternate solution 1
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 2
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)
```

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

### Q3: 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.

Given a positive integer `amount`

, a set of coins makes change for `amount`

if
the sum of the values of the coins is `amount`

. 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 recursive function
`count_change`

that takes a positive integer `amount`

and returns the number of
ways to make change for `amount`

using these coins of the future.

Hint:Refer the implementation of`count_partitions`

for an example of how to count the ways to sum up to an amount with smaller parts. If you need to keep track of more than one value across recursive calls, consider writing a helper function.

```
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
>>> from construct_check import check
>>> check(HW_SOURCE_FILE, 'count_change', ['While', 'For'])
True
"""
return count_using(amount, max_coin(amount))
def max_coin(amount):
coin = 1
while coin * 2 < amount:
coin *= 2
return coin
def count_using(amount, largest_coin):
if amount == 0:
return 1
elif amount < 0:
return 0
elif largest_coin == 0:
return 0
else:
with_largest = count_using(amount - largest_coin, largest_coin)
without_largest = count_using(amount, largest_coin // 2)
return with_largest + without_largest
```

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, so we need to create a helper function that takes in two arguments and also create another helper function to find the max coin. - Partition size is not linear, but rather multiples of two. To get the next partition you need to divide by two instead of subtracting one.

One other implementation detail here is that we enforce a *maximum* partition
size, rather than a *minimum* coin. Many students attempted to
start at 1 and work there way up. That will also work, but is less similar
to `count_partitions`

. As long as there is some ordering on the coins being enforced, we ensure we cover all the combinations of coins without any duplicates.

See the walkthrough for a more thorough explanation and a visual of the recursive calls. Video walkthrough: https://youtu.be/EgZJPNFnoxM.

### Q4: Towers of Hanoi

A classic puzzle called the Towers of Hanoi is a game that consists of three
rods, and a number of disks of different sizes which can slide onto any rod.
The puzzle starts with `n`

disks in a neat stack in ascending order of size on
a `start`

rod, the smallest at the top, forming a conical shape.

The objective of the puzzle is to move the entire stack to an `end`

rod,
obeying the following rules:

- Only one disk may be moved at a time.
- Each move consists of taking the top (smallest) disk from one of the rods and sliding it onto another rod, on top of the other disks that may already be present on that rod.
- No disk may be placed on top of a smaller disk.

Complete the definition of `move_stack`

, which prints out the steps required to
move `n`

disks from the `start`

rod to the `end`

rod without violating the
rules. The provided `print_move`

function will print out the step to move a
single disk from the given `origin`

to the given `destination`

.

Hint:Draw out a few games with various`n`

on a piece of paper and try to find a pattern of disk movements that applies to any`n`

. In your solution, take the recursive leap of faith whenever you need to move any amount of disks less than`n`

from one rod to another.

```
def print_move(origin, destination):
"""Print instructions to move a disk."""
print("Move the top disk from rod", origin, "to rod", destination)
def move_stack(n, start, end):
"""Print the moves required to move n disks on the start pole to the end
pole without violating the rules of Towers of Hanoi.
n -- number of disks
start -- a pole position, either 1, 2, or 3
end -- a pole position, either 1, 2, or 3
There are exactly three poles, and start and end must be different. Assume
that the start pole has at least n disks of increasing size, and the end
pole is either empty or has a top disk larger than the top n start disks.
>>> move_stack(1, 1, 3)
Move the top disk from rod 1 to rod 3
>>> move_stack(2, 1, 3)
Move the top disk from rod 1 to rod 2
Move the top disk from rod 1 to rod 3
Move the top disk from rod 2 to rod 3
>>> move_stack(3, 1, 3)
Move the top disk from rod 1 to rod 3
Move the top disk from rod 1 to rod 2
Move the top disk from rod 3 to rod 2
Move the top disk from rod 1 to rod 3
Move the top disk from rod 2 to rod 1
Move the top disk from rod 2 to rod 3
Move the top disk from rod 1 to rod 3
"""
assert 1 <= start <= 3 and 1 <= end <= 3 and start != end, "Bad start/end"
if n == 1:
print_move(start, end)
else:
other = 6 - start - end
move_stack(n-1, start, other)
print_move(start, end)
move_stack(n-1, other, end)
```

Use Ok to test your code:

`python3 ok -q move_stack`

To solve the Towers of Hanoi problem for `n`

disks, we need to do three
steps:

- Move everything but the last disk (
`n-1`

disks) to someplace in the middle (not the start nor the end rod). - Move the last disk (a single disk) to the end rod. This must occur after step 1 (we have to move everything above it away first)!
- Move everything but the last disk (the disks from step 1) from the middle on top of the end rod.

We take advantage of the fact that the recursive function `move_stack`

is
guaranteed to move `n`

disks from `start`

to `end`

while obeying the rules
of Towers of Hanoi. The only thing that remains is to make sure that we
have set up the playing board to make that possible.

Since we move a disk to end rod, we run the risk of `move_stack`

doing an
improper move (big disk on top of small disk). But since we're moving the
biggest disk possible, nothing in the `n-1`

disks above that is bigger.
Therefore, even though we do not explicitly state the Towers of Hanoi
constraints, we can still carry out the correct steps.

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

### Q5: 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`

.

```
def replace_leaf(t, old, new):
"""Returns a new tree where every leaf value equal to old has
been replaced with new.
>>> yggdrasil = tree('odin',
... [tree('balder',
... [tree('thor'),
... tree('loki')]),
... tree('frigg',
... [tree('thor')]),
... tree('thor',
... [tree('sif'),
... tree('thor')]),
... tree('thor')])
>>> laerad = copy_tree(yggdrasil) # copy yggdrasil for testing purposes
>>> print_tree(replace_leaf(yggdrasil, 'thor', 'freya'))
odin
balder
freya
loki
frigg
freya
thor
sif
freya
freya
>>> laerad == yggdrasil # Make sure original tree is unmodified
True
"""
if is_leaf(t) and label(t) == old:
return tree(new)
else:
bs = [replace_leaf(b, old, new) for b in branches(t)]
return tree(label(t), bs)
```

Use Ok to test your code:

`python3 ok -q replace_leaf`

## Extra questions

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

### Q6: Quine

Write a one-line program that prints itself, using only the following features of the Python language:

- Number literals
- Assignment statements
- String literals that can be expressed using single or double quotes
- The arithmetic operators
`+`

,`-`

,`*`

, and`/`

- The built-in
`print`

function - The built-in
`eval`

function, which evaluates a string as a Python expression - The built-in
`repr`

function, which returns an expression that evaluates to its argument

You can concatenate two strings by adding them together with `+`

and repeat a
string by multipying it by an integer. Semicolons can be used to separate
multiple statements on the same line. E.g.,

```
>>> c='c';print('a');print('b' + c * 2)
a
bcc
```

Hint: Explore the relationship between single quotes, double quotes, and the
`repr`

function applied to strings.

A program that prints itself is called a Quine. Place your solution in the multi-line string named `quine`

.

*Note*: No tests will be run on your solution to this problem.

### Q7: Church numerals

The logician Alonzo Church invented a system of representing non-negative integers entirely using functions. The purpose was to show that functions are sufficient to describe all of number theory: if we have functions, we do not need to assume that numbers exist, but instead we can invent them.

Your goal in this problem is to rediscover this representation known as *Church
numerals*. Here are the definitions of `zero`

, as well as a function that
returns one more than its argument:

```
def zero(f):
return lambda x: x
def successor(n):
return lambda f: lambda x: f(n(f)(x))
```

First, define functions `one`

and `two`

such that they have the same behavior
as `successor(zero)`

and `successsor(successor(zero))`

respectively, but *do
not call successor in your implementation*.

Next, implement a function `church_to_int`

that converts a church numeral
argument to a regular Python integer.

Finally, implement functions `add_church`

, `mul_church`

, and `pow_church`

that
perform addition, multiplication, and exponentiation on church numerals.

```
def one(f):
"""Church numeral 1: same as successor(zero)"""
return lambda x: f(x)
def two(f):
"""Church numeral 2: same as successor(successor(zero))"""
return lambda x: f(f(x))
three = successor(two)
def church_to_int(n):
"""Convert the Church numeral n to a Python integer.
>>> church_to_int(zero)
0
>>> church_to_int(one)
1
>>> church_to_int(two)
2
>>> church_to_int(three)
3
"""
return n(lambda x: x + 1)(0)
def add_church(m, n):
"""Return the Church numeral for m + n, for Church numerals m and n.
>>> church_to_int(add_church(two, three))
5
"""
return lambda f: lambda x: m(f)(n(f)(x))
def mul_church(m, n):
"""Return the Church numeral for m * n, for Church numerals m and n.
>>> four = successor(three)
>>> church_to_int(mul_church(two, three))
6
>>> church_to_int(mul_church(three, four))
12
"""
return lambda f: m(n(f))
def pow_church(m, n):
"""Return the Church numeral m ** n, for Church numerals m and n.
>>> church_to_int(pow_church(two, three))
8
>>> church_to_int(pow_church(three, two))
9
"""
return n(m)
```

Use Ok to test your code:

```
python3 ok -q church_to_int
python3 ok -q add_church
python3 ok -q mul_church
python3 ok -q pow_church
```

Church numerals are a way to represent non-negative integers via
repeated function application. The definitions of `zero`

, `one`

, and
`two`

show that each numeral is a function that takes a function and
repeats it a number of times on some argument `x`

.

The `church_to_int`

function reveals how a Church numeral can be mapped
to our normal notion of non-negative integers using the increment
function.

Addition of Church numerals is function composition of the functions of
`x`

, while multiplication is composition of the functions of `f`

.