Fundamentals of Computer Science I: Media Computing (CS151.02 2007F)

Exam 3: Algorithms and Beyond


Distributed: Friday, 30 November 2007

Due: Friday, 7 December 2007

Preliminaries

Exam format

There are four problems on the exam. Some problems have subproblems. Each problem is worth twenty-five (25) points. The point value associated with a problem does not necessarily correspond to the complexity of the problem or the time required to solve the problem.

This is a take-home examination. You may use any time or times you deem appropriate to complete the exam, provided you return it to me by the due date.

I expect that someone who has mastered the material and works at a Moderate rate should have little trouble completing the exam in a reasonable amount of time. In particular, this exam is likely to take you about four to six hours, depending on how well you've learned topics and how fast you work. You should not work more than eight hours on this exam. Stop at eight hours and write “There's more to life than CS” and you will earn at least 80 points on this exam.

I would also appreciate it if you would write down the amount of time each problem takes. Each person who does so will earn two points of extra credit. Since I worry about the amount of time my exams take, I will give two points of extra credit to the first two people who honestly report that they've spent at least five hours on the exam or completed the exam. (At that point, I may then change the exam.)

Academic honesty

This examination is open book, open notes, open mind, open computer, open Web. However, it is closed person. That means you should not talk to other people about the exam. Other than as restricted by that limitation, you should feel free to use all reasonable resources available to you. As always, you are expected to turn in your own work. If you find ideas in a book or on the Web, be sure to cite them appropriately.

Although you may use the Web for this exam, you may not post your answers to this examination on the Web. And, in case it's not clear, you may not ask others (in person, via email, via IM, by posting a please help message, or in any other way) to put answers on the Web.

Because different students may be taking the exam at different times, you are not permitted to discuss the exam with anyone until after I have returned it. If you must say something about the exam, you are allowed to say “This is among the hardest exams I have ever taken. If you don't start it early, you will have no chance of finishing the exam.” You may also summarize these policies. You may not tell other students which problems you've finished. You may not tell other students how long you've spent on the exam.

You must include both of the following statements on the cover sheet of the examination.

  1. I have neither received nor given inappropriate assistance on this examination.
  2. I am not aware of any other students who have given or received inappropriate assistance on this examination.

Please sign and date each statement. Note that the statements must be true; if you are unable to sign either statement, please talk to me at your earliest convenience. You need not reveal the particulars of the dishonesty, simply that it happened. Note also that inappropriate assistance is assistance from (or to) anyone other than Professor Rebelsky (that's me) or Professor Davis.

Presenting your work

You must present your exam to me in two forms: both physically and electronically. That is, you must write all of your answers using the computer, print them out, number the pages, put your name on the top of every page, and hand me the printed copy. You must also email me a copy of your exam. You should create the emailed version by copying the various parts of your exam and pasting them into an email message. In both cases, you should put your answers in the same order as the problems. Failure to name and number the printed pages will lead to a penalty of two points. Failure to turn in both versions may lead to a much worse penalty.

In many problems, I ask you to write code. Unless I specify otherwise in a problem, you should write working code and include examples that show that you've tested the code. Do not include images; I should be able to regenerate those.

Unless I explicitly ask you to document your procedures, you need not write introductory comments.

Just as you should be careful and precise when you write code and documentation, so should you be careful and precise when you write prose. Please check your spelling and grammar. Since I should be equally careful, the whole class will receive one point of extra credit for each error in spelling or grammar you identify on this exam. I will limit that form of extra credit to five points.

I will give partial credit for partially correct answers. You ensure the best possible grade for yourself by emphasizing your answer and including a clear set of work that you used to derive the answer.

Getting help

I may not be available at the time you take the exam. If you feel that a question is badly worded or impossible to answer, note the problem you have observed and attempt to reword the question in such a way that it is answerable. If it's a reasonable hour (before 10 p.m. and after 8 a.m.), feel free to try to call me in the office (269-4410) or at home (236-7445).

I will also reserve time at the start of classes next week to discuss any general questions you have on the exam.

Problems

Problem 1: Building Files from Files

Topics: Higher-order procedures, files, map

As you may recall, map is one of the most popular and most useful higher-order procedures. In its typical form, map takes two parameters, a unary procedure and a list. map then creates a new list by applying the procedure to each element of the original list.

You may also recall that files are useful when we have collections of values that will not necessarily fit in memory or that we want to preserve. If we are collecting our data in files, it makes equal sense to write a version of map that works with files, rather than with lists.

Write a procedure, (file.map source proc dest), that reads each value from the file named by the string source, applies proc, and writes the result to the file named by dest.

For example, suppose the file rational-numbers.txt contains rational numbers between 0 and 1, like so:

0.1410427672 0.7246121521 0.2468009653 0.3293669477 0.1280985349 0.5556000506 0.3700089885 0.2961479627 0.641676477 0.36518387 

We want to convert each fraction into a percentage rounded to the nearest point. We should be able to do so using the file.map procedure.

(file.map "rational-numbers.txt"
          (lambda (n) (round (* 100 n)))
	  "percentages.txt")

This should produce the following output in the file percentages.txt.

14 72 25 33 13 56 37 30 64 37 

It is not acceptable to write this procedure by reading all the values from the file into a list, using map, and then writing the list back to a file. Your goal is to read, modify, and write each value in turn.

Note: Use read to read each value from the source file. Recall that read returns the special end-of-file object (determined by eof-object?) when you attempt to read beyond the end of a file.

Problem 2: Tallying, Revisited

Topics: Higher-order procedures, vectors, trees, tallying, recursion, documentation, testing

As you may recall, there are a number of instances in which we have wanted to tally, or count, the number of values in a list that meet some criteria. In the past, we've tallied odd numbers and bright colors, among other things.

The principle of writing general procedures, along with the technique of higher-order order programming, suggests that we should consider writing a more general tally procedure that tallies all the values that meet a particular predicate. (Of course, we should not name it tally, since it is then unclear whether it tallies values in a list, vector, or tree.)

a. Document a procedure, (list.tally lst pred?), that tallies all the values in lst for which pred? holds. Pay particular attention to preconditions and postconditions. [5 points]

b. Implement list.tally. [5 points]

c. Give a call to list.tally to tally all the even integers in a list, some-values. Note that some-values may contain values of any type; you should just tally the integers. [5 points]

d. Implement a procedure, (vector.tally vec pred?), that tallies all of the values in vec for which pred? holds. [5 points]

e. Implement a procedure, (tree.tally tree pred?), that tallies all of the values in the tree tree for which pred? holds. [5 points]

Problem 3: Sorting Named Objects

Topics: Sorting, keys and keyed values

As you may recall, we learned at least three forms of the generalized insertion sort procedure.

  • list.insertion-sort takes two parameters: a list to sort and a may-precede? predicate that compares two values and determines whether the first may precede the second in the sorted list.
  • list.keyed-insertion-sort takes three parameters: a list to sort, a get-key predicate that extracts keys from values in the list, and a may-precede? predicate that compares two keys and determines whether the first may precede the second.
  • vector.insertion-sort! takes two parameters: a vector to sort and a may-precede? predicate. In this case, the sorting is in place, modifying the vector to be sorted.

Suppose we have a list of named objects, such as the following:

(define drawing
  (list (list "circ1" "ellipse" "red" 10 10 80 80)
        (list "thin" "ellipse" "blue" 10 80 300 10)
        (list "tall" "rectangle" "green" 80 5 100 2)
        (list "ys1" "rectangle" "yellow" 0 50 10 10)
        (list "ys2" "rectangle" "yellow" 0 50 20 20)
        (list "ys3" "rectangle" "yellow" 0 55 30 30)
        (list "ys4" "rectangle" "yellow" 0 60 40 40)
        (list "ys5" "rectangle" "yellow" 0 65 50 50)
        (list "ys6" "rectangle" "yellow" 0 70 60 60)
        (list "rc" "ellipse" "red" 100 100 30 30)
        (list "oc" "ellipse" "orange" 90 110 30 30)
        (list "yc" "ellipse" "yellow" 80 120 30 30)
        (list "gc" "ellipse" "green" 80 130 30 30)
        (list "bc" "ellipse" "blue" 90 140 30 30)
        (list "ic" "ellipse" "indigo" 100 150 30 30)
        (list "vc" "ellipse" "violet" 110 160 30 30)
        (list "last" "rectangle" "white" 0 0 1 1)))

Sorting this list by name is fairly easy with list.keyed-insertion-sort. We grab the name with car and compare names with string-ci<=?.

> (list.keyed-insertion-sort drawing car string-ci<=?)

It's a bit more complex to sort this list by name using the more general list.insertion-sort. In this case, we need to make the car part of the comparator.

> (list.insertion-sort drawing (lambda (o1 o2) (string-ci<=? (car o1) (car o2))))

Similarly, sorting this list by left margin is relatively easy using list.keyed-insertion-sort. In this case, the key is the left margin (element 3) and we compare keys numerically.

> (list.keyed-insertion-sort drawing (lambda (object) (list-ref object 3)) <=)

We might also express this a bit more concisely with r-s (our shorthand for right-section).

> (list.keyed-insertion-sort drawing (r-s list-ref 3) <=)

But what if we want to sort using more complex criteria? That's your goal to figure out for this problem.

a. Write a command (that you could type in the interactions window) to use list.keyed-insertion-sort to sort drawing by the size of an object, from smallest to largest. For simplicity, assume that we compute the size of an object by multiplying its width and height. [10 points]

> (list.keyed-insertion-sort drawing (lambda (obj) _____) ____)

b. Write a command (that you could type in the interactions window) to use list.insertion-sort to sort drawing alphabetically by color name. If multiple objects have the same color, they should be ordered by type (ellipses before rectangles), and then by name. [10 points]

> (list.insertion-sort drawing (lambda (obj1 obj2) _____))

As the description suggests, you need to first compare by color, then by type, and finally by name. For example, ("alpha" "rectangle" "black") comes before ("zeta" "ellipse" "white") because "black" alphabetically precedes "white". However, ("alpha" "rectangle" "white") comes after ("zeta" "ellipse" "white") because the two colors are the same, and ellipses come before rectangles. Finally, ("alpha" "ellipse" "white") comes before ("zeta" "ellipse" "white") because the colors and shapes are the same, and "alpha" precedes "zeta".

Note: You may find it useful to define a helper procedure to do the comparison. It is certainly fine to do so during development. However, in the end, you should answer this questions with a single command (which may involve a let statement).

c. Write a command (that you could type in the interactions window) to sort drawing by brightness of color, from darkest to lightest. (Note that you're starting with color names, so you'll have to convert those to RGB colors and then compute the brightness of those colors.) [5 points]

Problem 4: Writing Compressed Images

Topics: Files, representing images

As you may recall, we devised, but did not implement, a technique for more efficiently storing images. In this technique, which is called run-length encoding, when you have a sequence of pixels of the same color, you write one entry for all the pixels, rather than a separate entry for each pixel. That is, suppose the first five pixels in an image are purple. We would write the color purple and then the number 5. To read the image back from the file, we repeatedly read the color and the number of repetitions, and fill in that many pixels.

For example, consider a mostly-black 5x5 image with a single white pixel in the center. This image has 12 black pixels in sequence (five in the first row, five in the second row, two in the third row), one white pixel, and then another 12 black pixels (two more in the third row, five in the forth row, and five in the fifth row). We might represent this image as

5 5
0 0 0 12
255 255 255 1
0 0 0 12

Of course, nothing (other than our desire to produce the smallest file possible) says that we have to continue pixels from one row to the next. Hence, we could also represent the image as

5 5
0 0 0 5
0 0 0 5
0 0 0 2
255 255 255 1
0 0 0 2
0 0 0 5
0 0 0 5

In fact, we could even represent each pixel separately (which gives us a larger file than we would have otherwise).

5 5
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
255 255 255 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1

How can we read such a file back into an image? We might use a procedure like the following.

;;; Procedure:
;;;   image.read-compressed
;;; Parameters:
;;;   filename, a string
;;; Purpose:
;;;   Read a compressed image from the given file.
;;; Produces:
;;;   image, an image
;;; Preconditions:
;;;   filename names a valid file.
;;;   The file has the following form:
;;;     (1) an integer that specifies the width of the image
;;;     (2) an integer that specifies the height of the image
;;;     (3) a sequence of four-integer sequences, each of which specifies
;;;         red, green, blue, and repetitions.  For example, "255 0 0 10" means
;;;         "the next ten pixels are red".
;;; Postconditions:
;;;   The image corresponds to the description in the file.
;;; Note:
;;;   Internally, positions are represented as (col.row) pairs.
(define image.read-compressed
  (lambda (filename)
    (let* ((inport (open-input-file filename))
           (width (int.read inport))
           (height (int.read inport))
           (image (image.new width height))
           ; nextpos takes a (col.row) pair and produces the next
           ; (col.row) pair in the image.  If there is no next pair,
           ; nextpos returns #f.
           (nextpos (lambda (pos)
                      (cond
                        ; If we're not yet at the last column, advance
                        ; to the next column.
                        ((< (car pos) (- width 1))
                         (cons (+ 1 (car pos)) (cdr pos)))
                        ; We're at the last column.  If we're not at the
                        ; last row, advance to the beginning of the next
                        ; row
                        ((< (cdr pos) (- height 1))
                         (cons 0 (+ 1 (cdr pos))))
                        ; Well, it appears we've run out of rows and cols.
                        (else
                         #f)))))
      (let kernel ((pos (cons 0 0))
                   (color (rgb.read inport))
                   (count (int.read inport)))
        (cond
          ; No positions left?  We're done.
          ((not pos)
           (close-input-port inport)
           image)
          ; No colors left?  Read another one.
          ((zero? count)
           (kernel pos (rgb.read inport) (int.read inport)))
          ; Otherwise, set the color in the current position and move
          ; on to the next.
          (else
           (image.set-pixel! image (car pos) (cdr pos) color)
           (kernel (nextpos pos) color (- count 1))))))))

How would we write these files? It depends on whether we want to be as concise as possible or are willing to let the file be larger. If we're willing to write an uncompressed version, using a count of 1 per color, we can use a technique like that we've previously used to write images. If we prefer to do a row at a time, we might keep track of the current color on the row and the number of pixels. If we want the most compressed version, we need to work with multiple rows. Again, we keep track of the most recent color encountered and the number of pixels with that color. We then look at the next pixel, whether or not it's on the same row. If the next pixel is the same color, we increment the count for that color and go on. If the next pixel is a different color, we write the previous color and count, and continue with the new color and a count of 1. When we run out of pixels to write, we write the final color and count.

a. Write a procedure, (image.write-uncompressed image filename), that writes an image to a file using run-length encoding with the naive technique (that is, write each pixel separately with a count of 1). This procedure should create files that can be read by image.read-compressed. [15 points]

b. Write a procedure, (image.write-semi-compressed image filename), that writes an image to a file using run-length encoding with the row-by-row technique. [5 points]

c. Write a procedure, (image.write-compressed image filename), that writes an image to a file using run-length encoding with the multi-row technique. [5 points]

In each case, you'll benefit from rgb.write and rgb.read, defined as follows:

(define rgb.write
  (lambda (color port)
    (int.write (rgb.red color) port)
    (int.write (rgb.green color) port)
    (int.write (rgb.blue color) port)))

(define rgb.read
  (lambda (port)
    (let* ((red (int.read port))
           (green (int.read port))
           (blue (int.read port)))
       (if (eof-object? blue) 
           blue
           (rgb.new red green blue)))))

During testing, you may want to use the following versions of int.write and int.read.

(define int.write
  (lambda (int port)
    (write int port)
    (display " " port)))

(define int.read
  (lambda (port)
    (read port)))

However, in the end, we will use the versions of these procedures that produce a more compact representation.

(define int.write
  (lambda (int port)
    (write-char (integer->char (+ int 1)) port)))

(define int.read
  (lambda (port)
    (let ((ch (read-char port)))
      (if (eof-object? ch)
          ch
          (- (char->integer ch) 1)))))

Some questions and answers

Problem 1

Problem 2

Where have we seen the tally procedure before?

We wrote a test suite for a version of list.tally.

For problem 2(c), my version of list.tally crashes when I give even? as the predicate. Is it broken?

No, your list.tally isn't broken---it is doing exactly what you would expect it to do. even? always crashes when it is given a parameter that is not an integer. You will need to write a new predicate that doesn't crash when it is given values other than integers.

Problem 3

Is there a specific procedure you want us to use to compute brightness?

(define rgb.brightness
  (lambda (color)
    (+ (* 0.30 (rgb.red color))
       (* 0.59 (rgb.green color))
       (* 0.11 (rgb.blue color)))))

Problem 4

Errata

Here you will find errors of spelling, grammar, and design that students have noted. Remember, each error found corresponds to a point of extra credit for everyone. I usually limit such extra credit to five points. However, if I make an astoundingly large number of errors, then I will provide more extra credit.

  • The first my precede” should be “may precede”. [YX, 1 point]
  • Superfluous “a” in “It is not acceptable to write this a procedure by ...”. [HL, 1 point]
  • Problem 2 concerns lists, vectors, and trees, not lists, vectors, and files. [PB, 1 point]

Creative Commons License

Samuel A. Rebelsky, rebelsky@grinnell.edu

Copyright 2007 Janet Davis, Matthew Kluber, and Samuel A. Rebelsky. (Selected materials copyright by John David Stone and Henry Walker and used by permission.)

This material is based upon work partially supported by the National Science Foundation under Grant No. CCLI-0633090. Any opinions, findings, and conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Science Foundation.

This work is licensed under a Creative Commons Attribution-NonCommercial 2.5 License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/2.5/ or send a letter to Creative Commons, 543 Howard Street, 5th Floor, San Francisco, California, 94105, USA.