Lab 12: Final Review
Due by 11:59pm on Thursday, August 6.
Starter Files
Download lab12.zip.
Attendance
You need to submit the lab problems in addition to attending to get credit for lab.
If you miss lab for a good reason (such as sickness or a scheduling conflict) or you don't get checked in for some reason, email cs61a@berkeley.edu within one week to receive attendance credit.
Required Questions
This lab will be a collection of practice problems from some select topics covered in this course. Some topics that haven't been covered will be on the discussion worksheet.
Mutable Trees
Tree instance has two instance attributes:
labelis the value stored at the root of the tree.branchesis a list ofTreeinstances that hold the labels in the rest of the tree.
The Tree class (with its __repr__ and __str__ methods omitted) is defined as:
class Tree:
"""A tree has a label and a list of branches.
>>> t = Tree(3, [Tree(2, [Tree(5)]), Tree(4)])
>>> t.label
3
>>> t.branches[0].label
2
>>> t.branches[1].is_leaf()
True
"""
def __init__(self, label, branches=[]):
self.label = label
for branch in branches:
assert isinstance(branch, Tree)
self.branches = list(branches)
def is_leaf(self):
return not self.branches
To construct a Tree instance from a label x (any value) and a list of branches bs (a list of Tree instances) and give it the name t, write t = Tree(x, bs).
For a tree t:
- Its root label can be any value, and
t.labelevaluates to it. - Its branches are always
Treeinstances, andt.branchesevaluates to the list of its branches. t.is_leaf()returnsTrueift.branchesis empty andFalseotherwise.- To construct a leaf with label
x, writeTree(x).
Displaying a tree t:
repr(t)returns a Python expression that evaluates to an equivalent tree.str(t)returns one line for each label indented once more than its parent with children below their parents.
>>> t = Tree(3, [Tree(1, [Tree(4), Tree(1)]), Tree(5, [Tree(9)])])
>>> t # displays the contents of repr(t)
Tree(3, [Tree(1, [Tree(4), Tree(1)]), Tree(5, [Tree(9)])])
>>> print(t) # displays the contents of str(t)
3
1
4
1
5
9
Changing (also known as mutating) a tree t:
t.label = ychanges the root label ofttoy(any value).t.branches = nschanges the branches ofttons(a list ofTreeinstances).- Mutation of
t.brancheswill changet. For example,t.branches.append(Tree(y))will add a leaf labeledyas the right-most branch. - Mutation of any branch in
twill changet. For example,t.branches[0].label = ywill change the root label of the left-most branch toy.
>>> t.label = 3.0
>>> t.branches[1].label = 5.0
>>> t.branches.append(Tree(2, [Tree(6)]))
>>> print(t)
3.0
1
4
1
5.0
9
2
6
Here is a summary of the differences between the tree data abstraction implemented as a functional abstraction vs. implemented as a class:
| - | Tree constructor and selector functions | Tree class |
|---|---|---|
| Constructing a tree | To construct a tree given a label and a list of branches, we call tree(label, branches) |
To construct a tree object given a label and a list of branches, we call Tree(label, branches) (which calls the Tree.__init__ method). |
| Label and branches | To get the label or branches of a tree t, we call label(t) or branches(t) respectively |
To get the label or branches of a tree t, we access the instance attributes t.label or t.branches respectively. |
| Mutability | The functional tree data abstraction is immutable (without violating its abstraction barrier) because we cannot assign values to call expressions | The label and branches attributes of a Tree instance can be reassigned, mutating the tree. |
| Checking if a tree is a leaf | To check whether a tree t is a leaf, we call the function is_leaf(t) |
To check whether a tree t is a leaf, we call the method t.is_leaf(). This method can only be called on Tree objects. |
Visualizing Trees
If you would like some support with visualizing trees, please navigate
to code.cs61a.org, select Start Python Interpreter, and call autodraw().
Q1: Delete
Implement delete, which takes a Tree t and removes all non-root nodes labeled x.
The parent of each remaining node is its nearest ancestor that was not removed.
The root node is never removed, even if its label is x.
def delete(t, x):
"""Remove all nodes labeled x below the root within Tree t. When a non-leaf
node is deleted, the deleted node's children become children of its parent.
The root node will never be removed.
>>> t = Tree(3, [Tree(2, [Tree(2), Tree(2)]), Tree(2), Tree(2, [Tree(2, [Tree(2), Tree(2)])])])
>>> delete(t, 2)
>>> t
Tree(3)
>>> t = Tree(1, [Tree(2, [Tree(4, [Tree(2)]), Tree(5)]), Tree(3, [Tree(6), Tree(2)]), Tree(4)])
>>> delete(t, 2)
>>> t
Tree(1, [Tree(4), Tree(5), Tree(3, [Tree(6)]), Tree(4)])
>>> t = Tree(1, [Tree(2, [Tree(4), Tree(5)]), Tree(3, [Tree(6), Tree(2)]), Tree(2, [Tree(6), Tree(2), Tree(7), Tree(8)]), Tree(4)])
>>> delete(t, 2)
>>> t
Tree(1, [Tree(4), Tree(5), Tree(3, [Tree(6)]), Tree(6), Tree(7), Tree(8), Tree(4)])
"""
new_branches = []
for _________ in ________________:
_______________________
if b.label == x:
__________________________________
else:
__________________________________
t.branches = ___________________
Use Ok to test your code:
python3 ok -q delete
Recursion and Tree Recursion
Q2: Subsequences
A subsequence of a sequence s is a subset of elements from s, in the same
order they appear in s. Consider the list [1, 2, 3]. A few of its
subsequences are [], [1, 3], [2], and [1, 2, 3].
Write a function that takes in a list and returns all possible subsequences of that list. The subsequences should be returned as a list of lists, where each nested list is a subsequence of the original input.
In order to accomplish this, you might first want to write a function insert_into_all
that takes an item and a list of lists, adds the item to the beginning of each nested list,
and returns the resulting list.
def insert_into_all(item, nested_list):
"""Return a new list consisting of all the lists in nested_list,
but with item added to the front of each. You can assume that
nested_list is a list of lists.
>>> nl = [[], [1, 2], [3]]
>>> insert_into_all(0, nl)
[[0], [0, 1, 2], [0, 3]]
"""
"*** YOUR CODE HERE ***"
def subseqs(s):
"""Return a nested list (a list of lists) of all subsequences of S.
The subsequences can appear in any order. You can assume S is a list.
>>> seqs = subseqs([1, 2, 3])
>>> sorted(seqs)
[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]
>>> subseqs([])
[[]]
"""
if ________________:
________________
else:
________________
________________
Use Ok to test your code:
python3 ok -q subseqs
Q3: Non-Decreasing Subsequences
We want to write a function that takes a list and returns a list of lists, where each individual list is a subsequence of the original input.
However, we have a condition: we only want the subsequences for which
consecutive elements are nondecreasing. For example, [1, 3, 2] is a
subsequence of [1, 3, 2, 4], but since 2 < 3, this subsequence would not
be included in our result.
You may assume that the list passed in as s contains only nonnegative elements.
You may use the insert_into_all helper function.
def non_decrease_subseqs(s):
"""Return a nested list of all subsequences of S (a list of lists)
for which the elements of the subsequence are nondecreasing. The
subsequences can appear in any order. You can assume S is a list.
>>> seqs = non_decrease_subseqs([1, 3, 2])
>>> sorted(seqs)
[[], [1], [1, 2], [1, 3], [2], [3]]
>>> non_decrease_subseqs([])
[[]]
>>> seqs2 = non_decrease_subseqs([1, 1, 2])
>>> sorted(seqs2)
[[], [1], [1], [1, 1], [1, 1, 2], [1, 2], [1, 2], [2]]
"""
def subseq_helper(s, prev):
if not s:
return ____________________
elif s[0] < prev:
return ____________________
else:
a = ______________________
b = ______________________
return insert_into_all(________, ______________) + ________________
return subseq_helper(____, ____)
Use Ok to test your code:
python3 ok -q non_decrease_subseqs
Mutability
Q4: Common Players
Implement the function common_players. The common_players function takes in a roster dictionary that
maps players to their teams, and returns a new dictionary that maps teams to a list of players on that team.
The order of player names in the list does not matter.
def common_players(roster):
"""Returns a dictionary containing values along with a corresponding
list of keys that had that value from the original dictionary.
>>> full_roster = {
... "bob": "Team A",
... "barnum": "Team B",
... "beatrice": "Team C",
... "bernice": "Team B",
... "ben": "Team D",
... "belle": "Team A",
... "bill": "Team B",
... "bernie": "Team B",
... "baxter": "Team A"
... }
>>> player_dict = common_players(full_roster)
>>> type(player_dict) == dict
True
>>> for key, val in sorted(player_dict.items()):
... print(key, list(sorted(val)))
Team A ['baxter', 'belle', 'bob']
Team B ['barnum', 'bernice', 'bernie', 'bill']
Team C ['beatrice']
Team D ['ben']
"""
"*** YOUR CODE HERE ***"
Use Ok to test your code:
python3 ok -q common_players
Generators
Q5: Stair Ways
Imagine that you want to go up a staircase that has n steps,
where n is a positive integer. You can take either one or two steps each time you move.
Write a generator function stair_ways that yields all the different ways you can climb the staircase.
Each "way" of climbing a staircase can be represented by a list of 1s and 2s, where each number indicates whether you take one step or two steps at a time.
For example, for a staircase with 3 steps, there are three ways to climb it:
- You can take one step each time:
[1, 1, 1]. - You can take two steps then one step:
[2, 1]. - You can take one step then two steps:
[1, 2]..
Therefore, stair_ways(3) should yield [1, 1, 1], [2, 1], and [1, 2]. These can be yielded in any order.
Hint: Think about the problem recursively. If you're on some step n, which steps could you have just been on?
def stair_ways(n):
"""
Yield all the ways to climb a set of n stairs taking
1 or 2 steps at a time.
>>> list(stair_ways(0))
[[]]
>>> s_w = stair_ways(4)
>>> sorted([next(s_w) for _ in range(5)])
[[1, 1, 1, 1], [1, 1, 2], [1, 2, 1], [2, 1, 1], [2, 2]]
>>> list(s_w) # Ensure you're not yielding extra
[]
"""
"*** YOUR CODE HERE ***"
Use Ok to test your code:
python3 ok -q stair_ways
Object-Oriented Programming
Election
Let's implement a game called Election. In this game, two players compete to try and earn the most votes. Both players start with 0 votes and 100 popularity.
The two players alternate turns, and the first player starts. Each turn, the current player chooses an action. There are two types of actions:
- The player can debate, and either gain or lose 50 popularity. If the player
has popularity
p1and the other player has popularityp2, then the probability that the player gains 50 popularity ismax(0.1, p1 / (p1 + p2)). Note that themaxhere ensures that the probability is never lower than 0.1. - The player can give a speech. If the player has popularity
p1and the other player has popularityp2, then the player gainsp1 // 10votes and popularity and the other player losesp2 // 10popularity.
The game ends when a player reaches 50 votes, or after a total of 10 turns have been played (each player has taken 5 turns). Whoever has more votes at the end of the game is the winner!
Q6: Player
First, let's implement the Player class. Fill in the debate and speech
methods, that take in another Player other, and implement the correct
behavior as detailed above. Here are a few additional things to keep in mind:
- Each player carries a random number generator (the
random_funcinstance attribute), which is a function taking in no arguments that returns a random float between 0 and 1 when called. - In the
debatemethod, you should call therandom_funcfunction to get a random number. The player should gain 50 popularity if the random number is smaller than the probability described above, or lose 50 popularity otherwise. - Neither players' popularity should ever become negative. If this happens, set it equal to 0 instead.
### Phase 1: The Player Class
class Player:
"""
>>> random = make_test_random()
>>> p1 = Player('Hill', random)
>>> p2 = Player('Don', random)
>>> p1.popularity
100
>>> p1.debate(p2) # random() should return 0.0
>>> p1.popularity
150
>>> p2.popularity
100
>>> p2.votes
0
>>> p2.speech(p1)
>>> p2.votes
10
>>> p2.popularity
110
>>> p1.popularity
135
>>> p1.speech(p2)
>>> p1.votes
13
>>> p1.popularity
148
>>> p2.votes
10
>>> p2.popularity
99
>>> for _ in range(4): # 0.1, 0.2, 0.3, 0.4
... p1.debate(p2)
>>> p2.debate(p1)
>>> p2.popularity
49
>>> p2.debate(p1)
>>> p2.popularity
0
"""
def __init__(self, name, random_func):
self.name = name
self.votes = 0
self.popularity = 100
self.random_func = random_func
def debate(self, other):
"*** YOUR CODE HERE ***"
def speech(self, other):
"*** YOUR CODE HERE ***"
def choose(self, other):
return self.speech
Use Ok to test your code:
python3 ok -q Player
Q7: Game
Now, implement the Game class. Fill in the play method, which should
alternate between the two players, starting with p1, and have each player take
one turn at a time. The choose method in the Player class returns the
method, either debate or speech, that should be called to perform the
action.
In addition, fill in the winner method, which should return the
player with more votes, or None if the players are tied.
### Phase 2: The Game Class
class Game:
"""
>>> random = make_test_random()
>>> p1, p2 = Player('Hill',random), Player('Don', random)
>>> g = Game(p1, p2)
>>> winner = g.play()
>>> p1 is winner
True
>>> # Additional correctness tests
>>> winner is g.winner()
True
>>> g.turn
10
>>> p1.votes = p2.votes
>>> print(g.winner())
None
"""
def __init__(self, player1, player2):
self.p1 = player1
self.p2 = player2
self.turn = 0
def play(self):
while not self.game_over():
"*** YOUR CODE HERE ***"
return self.winner()
def game_over(self):
return max(self.p1.votes, self.p2.votes) >= 50 or self.turn >= 10
def winner(self):
"*** YOUR CODE HERE ***"
Use Ok to test your code:
python3 ok -q Game
Q8: New Players (Optional)
The choose method in the Player class is boring because it always returns
the speech method. Let's implement two new classes that inherit from Player,
but have more interesting choose methods.
Implement the choose method in the AggressivePlayer class, which returns the
debate method if the player's popularity is less than or equal to other's
popularity, and speech otherwise. Also implement the choose method in the
CautiousPlayer class, which returns the debate method if the player's
popularity is 0, and speech otherwise.
### Phase 3: New Players
class AggressivePlayer(Player):
"""
>>> random = make_test_random()
>>> p1, p2 = AggressivePlayer('Don', random), Player('Hill', random)
>>> g = Game(p1, p2)
>>> winner = g.play()
>>> p1 is winner
True
>>> # Additional correctness tests
>>> p1.popularity = p2.popularity
>>> p1.choose(p2) == p1.debate
True
>>> p1.popularity += 1
>>> p1.choose(p2) == p1.debate
False
>>> p2.choose(p1) == p2.speech
True
"""
def choose(self, other):
"*** YOUR CODE HERE ***"
Use Ok to test your code:
python3 ok -q AggressivePlayer
class CautiousPlayer(Player):
"""
>>> random = make_test_random()
>>> p1, p2 = CautiousPlayer('Hill', random), AggressivePlayer('Don', random)
>>> p1.popularity = 0
>>> p1.choose(p2) == p1.debate
True
>>> p1.popularity = 1
>>> p1.choose(p2) == p1.debate
False
>>> # Additional correctness tests
>>> p2.choose(p1) == p2.speech
True
"""
def choose(self, other):
"*** YOUR CODE HERE ***"
Use Ok to test your code:
python3 ok -q CautiousPlayer
Linked Lists
Q9: Two List
Implement a function two_list that takes in two lists and returns a linked list. The first list contains the
values that we want to put in the linked list, and the second list contains the number of each corresponding value.
Assume both lists are the same size and have a length of 1 or greater. Assume all elements in the second list
are greater than 0.
def two_list(vals, counts):
"""
Returns a linked list according to the two lists that were passed in. Assume
vals and counts are the same size. Elements in vals represent the value, and the
corresponding element in counts represents the number of this value desired in the
final linked list. Assume all elements in counts are greater than 0. Assume both
lists have at least one element.
>>> a = [1, 3]
>>> b = [1, 1]
>>> c = two_list(a, b)
>>> c
Link(1, Link(3))
>>> a = [1, 3, 2]
>>> b = [2, 2, 1]
>>> c = two_list(a, b)
>>> c
Link(1, Link(1, Link(3, Link(3, Link(2)))))
"""
"*** YOUR CODE HERE ***"
Use Ok to test your code:
python3 ok -q two_list
Scheme/Tail Recursion
Q10: Accumulate
Fill in the definition for the procedure accumulate, which joins the first
n natural numbers (ie. 1 to n, inclusive) according to the following parameters:
merger: a function of two argumentsstart: a number with which we start joiningn: the number of natural numbers to jointerm: a function of one argument that computes the nth term of a sequence
For example, we can find the product of all the numbers from 1 to 5 by
using the multiplication operator as the merger, and starting our
product at 1:
scm> (define (identity x) x)
scm> (accumulate * 1 5 identity) ; 1 * 1 * 2 * 3 * 4 * 5
120
We can also find the sum of the squares of the same numbers by using the
addition operator as the merger and square as the term:
scm> (define (square x) (* x x))
scm> (accumulate + 0 5 square) ; 0 + 1^2 + 2^2 + 3^2 + 4^2 + 5^2
55
scm> (accumulate + 5 5 square) ; 5 + 1^2 + 2^2 + 3^2 + 4^2 + 5^2
60
You may assume that the merger will always be commutative: i.e. the order
of arguments do not matter.
(define (accumulate merger start n term)
'YOUR-CODE-HERE
)
Use Ok to unlock and test your code:
python3 ok -q accumulate -u
python3 ok -q accumulate
Q11: Tail Recursive Accumulate (Optional)
In a previous part of the homework, you implemented accumulate in scheme. As a reminder, accumulate merges
the first n natural numbers according to the parameters merger, start, n, and term.
You can refer to your implementation of accumulate as a reminder of what the function does and a refresher of its implementation.
Update your implementation of accumulate to be tail recursive. It
should still pass all the tests for "regular" accumulate!
You may assume that the input merger and term procedures are
properly tail recursive.
If your implementation for accumulate in the previous question is already
tail recursive, you may simply copy over that solution (replacing accumulate
with accumulate-tail as appropriate).
If you're running into an recursion depth exceeded error and you're using the staff interpreter, it's very likely your solution is not properly tail recursive.
We test that your solution is tail recursive by calling
accumulate-tailwith a very large input. If your solution is not tail recursive and does not use a constant number of frames, it will not be able to successfully run.
(define (accumulate-tail merger start n term)
'YOUR-CODE-HERE
)
Use Ok to test your code:
python3 ok -q accumulate-tail
Check Your Score Locally
You can locally check your score on each question of this assignment by running
python3 ok --score
This does NOT submit the assignment! When you are satisfied with your score, submit the assignment to Gradescope to receive credit for it.
Submit Assignment
Submit this assignment by uploading any files you've edited to the appropriate Gradescope assignment. Lab 00 has detailed instructions.
Correctly completing all questions is worth one point. Please ensure your TA has taken your attendance before leaving.