CS106A Midterm + Solution

Stanford, Winter 2024-25

This was quite a challenging exam, and we were pleased with how well the class did overall. The median was 86/100.

Instructions

# CS106A Exam Reference Reminder
# [square brackets] denote functions listed here that we have not used yet
# Exam questions will not depend on functions we have not used yet
Bit:
  bit = Bit(filename)
  bit.front_clear() bit.left_clear() bit.right_clear()
  bit.get_color()
  bit.move() bit.left() bit.right()
  bit.paint('red') [bit.erase()]
General functions:
  len() int() str() range() [list() sorted()]
String functions:
  isalpha() isdigit() isspace() isupper() islower()
  find() upper() lower() strip()
List functions:
  append() index() [extend() pop() insert()]
SimpleImage:
  # read filename
  image = SimpleImage(filename)
  # create blank image
  image = SimpleImage.blank(width, height)

  # foreach loop
  for pixel in image:

  # range/y/x loop
  for y in range(image.height):
      for x in range(image.width):
          pixel = image.get_pixel(x, y)
Grid 2D:
  grid = Grid.build([['row', '0'], ['row', '1']])
  grid.width, grid.height - properties
  grid.in_bounds(x, y) - True if in bounds
  grid.get(x, y) - returns contents at that x,y, or None if empty
  grid.set(x, y, value) - sets new value into grid at x,y

1. Short Answer (6 points)

a. Write what value comes from the Python expression:

>>> 1 + 2 + 3 * 4 - 1


>>> 24 % 5


>>> 3 ** 2


>>> s = 'Python'
>>> s[1:3]


b. What 3 numbers print when the caller() function runs? This code runs without error.

def foo(a, b):
    a *= 2
    b += 1
    return a + b


def caller():
    a = 1
    b = 2
    c = foo(b, a)
    print(a, b, c)
# What 3 numbers print?


2. Bit (6 points)

Bit is in a 1-high tunnel, facing the right side of the world. Move bit forward until an opening appears above, the entrance of a 1-wide vertical hole. Move bit into the hole. Move bit forward until an opening appears towards the left side of the world, the entrance of a second 1-high tunnel. Move bit forward in the tunnel until blocked. Use three while loops.

Bit before and after:
alt: bit 3 whiles picture

def loop3(filename):
    bit = Bit(filename)
    # Your code here




3. Image (16 points)

Given an image filename and ints a and b which are 1 or more. Produce a new out image with three features: (1) a horizontally flipped copy of the original image with the other rectangles around it. (1) two rectangles b pixels high and the same width as the original image, directly above and directly below the flipped image, with the lower rectangle painted aqua. (3) To the left and right of the rectangles and image, two blank vertical stripes, each a pixels wide. The stripes run the full height of the output. Please try to avoid writing too close to the right edge of the paper.
alt: blank rectangle, flipped image, 2 stripes

1. Write one line of code to create the out image.

2. Almost all of the code to write the flipped image is provided — looping over the original image, writing the colors at each "pixel" to "pixel_out". Write the one line of code which sets the "pixel_out" variable used by the later lines in this loop. It's ok if you need to put the parameters on multiple lines to make it fit.

3. Write the loops and other needed code to produce the aqua rectangle at the bottom. These are separate loops, not nested inside the earlier loops. To set each white pixel to aqua, the last line in the loop is like pixel_out.red = 0. We'll assume there is a return out as the last line.

def do_image(filename, a, b):
    image = SimpleImage(filename)
    #### 1. Create "out" image


    # Write flipped image to out
    for y in range(image.height):
        for x in range(image.width):
            pixel = image.get_pixel(x, y)
            #### 2. Set "pixel_out"




            pixel_out.red = pixel.red
            pixel_out.green = pixel.green
            pixel_out.blue = pixel.blue
    #### 3. Fill aqua rectangle







4. Strings (40 points)

a. Given a string s, return a list containing every alphabetic char in s. Omit all the other chars. So for example 'Ab2$c' returns ['A', 'b', 'c']

'Ab2$c' ->         | def alphas(s):
  ['A', 'b', 'c']  |
'!!z!!z' ->        |
  ['z', 'z']       |
'@' ->
  []


b. Given a string s and an "omit" list of lowercase chars like ['b', 'x', 'a']. Build and return a version of s, omitting all the chars that appear in the omit list. The logic should not be case-sensitive, so if an 'e' is in the omit list, then both 'e' and 'E' should be omitted from the result.

'Kitten', ['e', 't']  | def without(s, omit):
  -> 'Kin'            |
'Kitten', ['k', 'x'   |
  -> 'itten'          |
'KITTEN', ['t', 'i']
  -> 'KEN'
'Hi', ['i', 'h', 'x']
  -> ''





c. Given a string s and a non-negative int n. (1) If there is a digit at the index 2 * n, then return double the digit number. (2) Otherwise, if the chars at indexes n and 2 * n are both alphabetic, then return the string length 2 made of those chars. Otherwise return the empty string.

'abcd9', 2  -> 18     | def special_n(s, n):
'a12de', 1  -> 4      |
'abcde', 1  -> 'bc'   |
'ab5de', 1  -> 10
'abcde', 3  -> ''





d. Find the first '@' char in s. If it is present, and the 2nd char after the '@' is a digit, then return the value of that digit squared, e.g. 'abc@x3y' returns 9. Otherwise, if the '@' with 2nd char digit is not there, but the first char in the string is a digit, then return the value of that digit squared. In all other cases, return -1. Use s.find().

'abc@x3y' -> 9  |  def at2(s):
'a@12'    -> 4  |
'abc@xy'  -> -1 |
'abc@5y'  -> -1 |
'abc@'    -> -1
'2abc@xx' -> 4
'xabc@xx' -> -1
'' -> -1














e. Given a string s, find the first '##' in s. Then find a second, non-overlapping '##' that comes after the first. If there are not two '##', return the empty string ''. Otherwise, consider the substring between the two '##' , such as the substring '123@ab' in 'x@##123@ab##xx'. If there is an '@' in the substring, then return a version of the substring with the parts before and after the '@' swapped, so '123@ab' returns 'ab@123'. If there is no '@' in the substring, simply return the substring unchanged. Use s.find() and slices.

'x@##123@ab##xx' |  def hash_at(s):
  -> 'ab@123'    |
'x@##Hello##xx'  |
  -> 'Hello'     |
'##syz#x#'
  -> ''
'12@ab##'
  -> ''



5. Downward Squirrel (18 points)

In this problem, every square is one of these four values: empty None, squirrel 's', nut 'n', or tree 't'. As usual, you can get full credit for each part independently of what you write on the other parts. Write the code for the function downward_squirrel(grid, x, y, n) assuming the parameters x, y represent an in-bounds square that contains a squirrel, and the parameter n is an int, 1 or greater. Here are the conditions for the downward squirrel action:

1. Consider the square in the same row as the x, y squirrel, and n squares to the left - call this square A. Consider the square in the row immediately below the squirrel and n squares to the right - call this square B.

2. If squares A and B are non-empty and each contain the same value as the other, and the square immediately below the squirrel is empty, then the downward-squirrel action happens: the squirrel moves to the square immediately below.

3. Otherwise, the grid is left unchanged.

def downward_squirrel(grid, x, y, n):



















(b) Add the two lines to complete the following Doctest for a call to downward_squirrel() where the action succeeds and the squirrel moves. The first Grid.build() line is provided which builds a small grid suitable for this Doctest.

>>> grid = Grid.build([['t', 's', 's'], ['t', None, 't']])
>>>


6. Encryption (14 points)

This problem resembles the homework encryption, but with some differences. As a simplification, we will not handle uppercase chars. For this encryption, there is an extra shift list, the same length as the source list, which contains non-negative int values. Here are example lists, with the index numbers shown:

alt: xxx source ['a', 'b', 'c', 'd'] and slug ['@', '*', 'w', 'e', 'r', 'k']

If the input char is in the source, compute the "output" index of the output char as follows: The output index is the sum of the input char's index in the source list plus the number in the shift list at the same index. So for example, for the input char 'b', its index is 1, and its shift value is 2, so its output char in the slug is 1 + 2 = 3, e.g. 'e'. If the output index is too big for the slug, then the output char should be '!'. If the input char is not in the source, then the output char is the input char unchanged.

Encryption examples using above lists
'a' -> 'y' (0 + 1)
'b' -> 'e' (1 + 2)
'c' -> 'w' (2 + 0)
'd' -> '!' (3 + 4 too big)
'$' -> '$'
def encrypt_char(source, shift, slug, ch):
    # Your code here
    

Solutions

#### 1. Short Answer

14
4
9
'yt'

1 2 6


#### 2. Bit

def loop3(filename):
    bit = Bit(filename)

    # Find the hole to our left
    while not bit.left_clear():
        bit.move()
    # Into the hole
    bit.left()
    bit.move()
    # Find tunnel
    while not bit.left_clear():
        bit.move()
    # Face into tunnel and go
    bit.left()
    while bit.front_clear():
        bit.move()


#### 3. Image

# 1
out = SimpleImage.blank(image.width + 2 * a, image.height + 2 * b)

# 2 flipped image
pixel_out = out.get_pixel(a + image.width - 1 - x, b + y)

# 3 aqua area
for x in range(image.width):
    for y in range(b):
        pixel_out = out.get_pixel(a + x, b + image.height + y)
        pixel_out.red = 0


#### 4. Strings

## a
def alphas(s):
    result = []
    for ch in s:
        if ch.isalpha():
            result.append(ch)
    return result

## b
def without(s, omit):
    result = ''
    for ch in s:
        if ch.lower() not in omit:
            result += ch
    return result


## c
def special_n(s, n):
    if 2 * n < len(s) and s[2 * n].isdigit():
        return int(s[2 * n]) * 2
    
    if 2 * n < len(s) and s[n].isalpha() and s[2 * n].isalpha():
        return s[n] + s[2 * n]
    
    return ''
    

## d
def at2(s):
    at = s.find('@')
    if at + 2 < len(s) and s[at + 2].isdigit():
        return int(s[at + 2]) ** 2
    if 0 < len(s) and s[0].isdigit():  # check that [0] is in bounds
        return int(s[0]) ** 2
    return -1


## e
def hash_at(s):
    left = s.find('##')
    right = s.find('##', left + 2)  # + 2 is needed
    if left == -1 or right == -1:   # Actually works without left check
        return ''
    mid = s[left + 2:right]  # Substring into var is helpful
    at = mid.find('@')
    if at != -1:
        return mid[at + 1:] + '@' + mid[:at]
    return mid

    # -or- without var
    at = s.find('@', left + 2)
    if at != -1 and at < right:  # Subtle: avoid '@' beyond '##'
        return s[at + 1:right] + '@' + s[left + 2:at]
    return s[left + 2:right]
    # Some students had: at = s.find('@')
    # which gets most of the points, but has a bug with '@'
    # outside of the hashes.


#### 5 Grid
def downward_squirrel(grid, x, y, n):
    # A and B in bounds
    if grid.in_bounds(x - n, y) and grid.in_bounds(x + n, y + 1):
        # A and B the same
        if grid.get(x - n, y) == grid.get(x + n, y + 1):
            # Below squirrel is empty
            if grid.get(x, y + 1) == None:
                grid.set(x, y, None)
                grid.set(x, y + 1, 's')
            
    return grid

## Doctest
>>> grid = Grid.build([['t', 's', 's'], ['t', None, 't']])
>>> downward_squirrel(grid, 1, 0, 1)
[['t', None, 's']['t', 's', 't']]


#### 6 Encrypt Char
def encrypt_char(source, shift, slug, ch):
    if ch in source:
        idx = source.index(ch)
        out = idx + shift[idx]  # Output index into var
        if out < len(slug):   # Not too big
            return slug[out]
        return '!'  # Too big
    return ch