about summary refs log tree commit diff stats
path: root/js/games/nluqo.github.io/~bh/61a-pages/Volume2/Midterm3/mt3-3.soln.txt
diff options
context:
space:
mode:
authorelioat <elioat@tilde.institute>2023-08-23 07:52:19 -0400
committerelioat <elioat@tilde.institute>2023-08-23 07:52:19 -0400
commit562a9a52d599d9a05f871404050968a5fd282640 (patch)
tree7d3305c1252c043bfe246ccc7deff0056aa6b5ab /js/games/nluqo.github.io/~bh/61a-pages/Volume2/Midterm3/mt3-3.soln.txt
parent5d012c6c011a9dedf7d0a098e456206244eb5a0f (diff)
downloadtour-562a9a52d599d9a05f871404050968a5fd282640.tar.gz
*
Diffstat (limited to 'js/games/nluqo.github.io/~bh/61a-pages/Volume2/Midterm3/mt3-3.soln.txt')
-rw-r--r--js/games/nluqo.github.io/~bh/61a-pages/Volume2/Midterm3/mt3-3.soln.txt459
1 files changed, 459 insertions, 0 deletions
diff --git a/js/games/nluqo.github.io/~bh/61a-pages/Volume2/Midterm3/mt3-3.soln.txt b/js/games/nluqo.github.io/~bh/61a-pages/Volume2/Midterm3/mt3-3.soln.txt
new file mode 100644
index 0000000..20db87b
--- /dev/null
+++ b/js/games/nluqo.github.io/~bh/61a-pages/Volume2/Midterm3/mt3-3.soln.txt
@@ -0,0 +1,459 @@
+CS 61A		Solutions to sample midterm 3 #3
+
+1.  Box and pointer.
+
+(let ((x (list 1 2 3)))
+  (set-cdr! (cdr x) (cddr x))
+  x)
+
+(1 2 3) is printed.
+
+X-->[o|o]-->[o|o]==>[o|/]
+     |       |       |      
+     V       V       V      
+     1       2       3
+
+In this example the SET-CDR! call effectively does nothing, since it
+replaces the cdr of (cdr x) with the cddr -- the cdr of the cdr -- of X.
+The arrow drawn with = instead of - above is the one that's replaced with
+itself.
+
+
+(let ((x (list 1 2 3)))
+  (set-car! x (cddr x))
+  x)
+
+((3) 2 3) is printed.
+
+     +----------------+
+     |                |
+     |                V
+X-->[o|o]-->[o|o]-->[o|/]
+             |       |      
+             V       V      
+             2       3
+
+The set-car! invocation changes (car x), i.e., the first element of X.
+It's changed to (cddr x), which is a *sublist*, not just an element, of X.
+Thus the new first element is not 3, but (3).
+
+
+
+(let ((x (list 1 2 3)))
+  (set-car! (cdr x) (cdr x))
+  x)
+(1 ((((((((( ...  is printed.
+
+             +-+
+             | |
+             | V
+X-->[o|o]-->[o|o]-->[o|/]
+     |               |      
+     V               V      
+     1               3 
+
+Note that the arrow at the top of the diagram points *from the car* of the
+second pair, but points *to the entire pair* (because that's how pointers
+always work), not just to the cdr, even though that's where the arrowhead
+happens to be.
+
+There's still a 3 in this list, but it'll never be printed.
+
+
+
+(let ((x (list 1 2 3 4)))
+  (set-cdr! (cddr x) (cadr x))
+  x)
+
+(1 2 3 . 2) is printed.
+
+X-->[o|o]-->[o|o]-->[o|o]--> 2      
+     |       |       |      
+     V       V       V      
+     1       2       3
+
+In this example we change the cdr of a pair (because we're doing SET-CDR!) to
+point to a non-pair, (CADR X), the second element of X, which is 2.  Thus the
+result is an improper list, so there's a dot in the printed version.
+
+It doesn't matter, in the diagram, whether you have two 2s, as above, or have
+two arrows pointing to the same 2.  It *does* matter when the thing being
+pointed to is a pair; then there's a big difference between two pointer to the
+same pair, and two pairs that have the same values in them.
+
+
+Scoring: One point per printed representation; one point per diagram.
+
+
+2.  Local state.
+
+Does Louis's MEMOIZE give wrong answers?  YES.
+
+Explanation:  It has one table shared among all memoized functions,
+instead of a separate table per function.
+
+Example:
+
+[Louis has defined MEMO-FIB, as shown in the question.]
+
+> (memo-fib 4)
+3
+> (define memo-square (memoize (lambda (x) (* x x))))
+> (memo-square 4)
+3
+
+Since MEMO-FIB and MEMO-SQUARE share the same table, (memo-square 4)
+returns the value previously remembered from (memo-fib 4).
+
+Some students who checked NO explained why the answer is really YES, so
+we figured people misread the question, and went by your explanantion
+rather than by which choice you checked.
+
+Pretty much any explanation with a phrase like "a single table" or
+"universal table" or "global table" was accepted.  (It's not quite right
+to say "a table in the global environment"; the table is actually a local
+state variable of MEMOIZE.)
+
+Explanations that merely restate the form of Louis's change to the program
+were not accepted:  "The program gives wrong answers because Louis has moved
+the LET to outside of the LAMBDA."  You had to explain what *effect* that
+would have on the program's *behavior*.
+
+The example didn't necessarily have to take the form of a specific Scheme
+interaction as above, but did have to assert that two different functions are
+memoized.
+
+Scoring:
+
+4  correct
+2  correct explanation, no/bad example;
+   iffy explanation (like "global environment" discussed above)
+0  bad explanation (including saying that it doesn't give wrong answers)
+
+No odd scores were assigned.
+
+
+3.  Environment diagram.
+
+Bindings A=8 and B=9 go in the global environment.
+
+Then comes the LET, which is equivalent to an invocation of the procedure
+generated by an implicit LAMBDA:
+
+	((lambda (a f) (f 5))
+	 3
+	 (lambda (b) (+ a b)))
+
+As with any procedure invocation, **first all the subexpressions are
+evaluated!**  This evaluation takes place in the global environment -- we
+haven't invoked any procedures yet.
+
+Thus, two procedures are created whose right bubbles point to the global
+environment.  Only then do we invoke the (lambda (a f) ...) procedure, which
+creates a frame in which A is bound to 3, and F is bound to the
+(lambda (b) ...) procedure.  That new frame, of course, extends the global
+environment.
+
+With that frame as (the first frame of) the current environment, we can
+evaluate the body of the LET, which is (F 5).  We find a binding for F in
+the current environment; it's the (lambda (b) ...) procedure.  So we can
+invoke that with the argument 5.
+
+Since F's right bubble points to the global environment, that's what we
+extend with the new frame that binds B=5.  So when we evaluate F's body,
+which is (+ A B), we find the bindings A=8 and B=5.
+
+Thus, the final answer is 13.
+
+
+The overwhelmingly most common mistake was to think that the (lambda (b) ...)
+procedure's right bubble points to the A=3 frame, so the B=5 frame points
+there also, and the final value is 8.  You should know, though, that a LET
+binding can't depend on another binding in the same LET; for example, if the
+problem had been
+
+	(let ((a 3)
+	      (c (+ a b)))
+	  (+ a c))
+
+I hope most people would have known that C would be 8+9, using the global
+bindings for both A and B.  The fact that the computation using A happens
+inside a lambda body doesn't change the fact that the LET's binding for A
+isn't available to it.
+
+A few people drew the environment diagram that would give an answer of 8,
+but instead wrote 13 as their final answer, which means either that they
+copied someone else's final answer or that they *do* know how Scheme
+evaluates expressions, but don't make any connection between that and
+the environment model.  We graded this as if they'd said 8.
+
+A couple of people had procedure F's right bubble correctly pointing to the
+global environment, but nevertheless had the B=5 frame extending the A=3
+frame, using dynamic scope instead of lexical scope.  This gave the same
+wrong final answer of 8.
+
+Scoring:
+
+6  correct.
+3  answer was (or should have been) 8, as discussed above.
+0  even worse (too many frames, no arrows at all -- there weren't many).
+
+
+4.  List mutation
+
+(define a (list 'x))
+(define b (list 'x))
+(define c (cons a b))
+(define d (cons a b)) 
+
+(eq? a b)                    => #F
+
+	Each call to LIST generates new pairs (one new pair, in this case,
+	because each list has only one element).
+
+(eq? (car a) (car b))        => #T
+
+	Both CARs are the symbol X, and equal symbols are always EQ.
+
+(eq? (cdr a) (cdr b))        => #T
+
+	Both CDRs are the empty list, and there's only one empty list.
+
+(eq? c d)                    => #F
+
+	Each call to CONS generates a new pair.
+
+(eq? (cdr c) (cdr d))        => #T
+
+	Both CDRs are the pair named B -- the same pair.
+
+(define p a)
+(set-car! p 'squeegee)
+(eq? p a)                    => #T
+
+	P and A are two names for the same pair.  Mutating the pair
+	doesn't change the variable bindings.
+
+(define q a)
+(set-cdr! a q)
+(eq? q a)                    => #T
+
+	This one looks trickier because the pair now points to itself,
+	but it's really the same as the previous question: Q and A are
+	two names for the same pair.
+
+(define r a)
+(set! r 'squeegee)
+(eq? r a)                    => #F
+
+	This time we didn't mutate a pair; we changed the binding of
+	one of the variables.  So R and A now name two different things;
+	A is still a pair, but R is now a symbol.
+
+Score: 1/2 point each, rounded down to the nearest whole point.
+
+
+5.  Vector programming
+
+(define (ssort! vec)
+  (define (help start)
+    (if (= start (vector-length vec))
+	vec
+	(let ((smallest (subvec-min-start vec start)))
+	  (let ((temp (vector-ref vec smallest)))
+	    (vector-set! vec smallest (vector-ref vec start))
+	    (vector-set! vec start temp)
+	    (help (+ start 1))))))
+  (help 0))
+
+The key point to understand is that you need to walk through the vector,
+bringing the smallest element out to position 0, then the next-smallest to
+position 1, and so on, until you run out of elements.  Thus we have a helper
+procedure with an argument, START, whose value is the position that we're
+up to in the vector; it starts at 0 and is increased by 1 on each recursive
+invocation.
+
+The next important point is that you can't exchange two elements without
+using a temporary variable to remember one of them, hence the LET that
+creates the variable TEMP.  There are two nested LETs because the value of
+SMALLEST is needed to find the value of TEMP in this solution; I could have
+simplified things a little by remembering the other value in the swap instead:
+
+(define (ssort! vec)
+  (define (help start)
+    (if (= start (vector-length vec))
+	vec
+	(let ((smallest (subvec-min-start vec start))
+	      (temp (vector-ref vec start)))
+	  (vector-set! vec start (vector-ref vec smallest))
+	  (vector-set! vec smallest temp)
+	  (help (+ start 1))))))
+  (help 0))
+
+Scoring:
+
+6  correct
+5  trivial error (e.g. base case off by one)
+3  right structure, gets swap wrong
+2  serious algorithm confusion
+0  allocates new vector, uses lists, or incoherent
+
+
+6.  Concurrency
+
+(define (make-mutex)
+  (let ((in-use #f)
+	(s (make-serializer)))
+    (define (the-mutex m)
+      (cond ((eq? m 'acquire)
+	     (if ((s (lambda ()
+		       (if in-use
+			   #t
+			   (begin (set! in-use #t)
+				  #f)))))
+		 (the-mutex 'acquire)))     ; retry
+	    ((eq? m 'release)
+	     (set! in-use #f))))
+    the-mutex))
+
+The structure of the above is just like that of the MAKE-MUTEX in the book,
+except that when an atomic test-and-set operation is needed, it's done by
+including the (IF IN-USE ...) and the (SET! IN-USE #T) within the same
+serialized procedure, rather than by relying on a TEST-AND-SET! primitive.
+
+Many people found the problem deeply confusing.  You learned that
+serialization has three levels of abstraction: the hardware support,
+the critical section mechanism (mutex) based on the hardware, and
+the more abstract level in which procedures are protected rather than
+sequences of instructions.  So what does it mean to define a mutex
+in terms of a serializer?  But actually this could happen.  Suppose
+you are using a language such as Java, with high-level serialization
+built in (and presumably implemented using hardware support), to write
+an operating system that is supposed to provide a mutex capability to
+its users.  You would then write something equivalent to what this
+problem asks for.  What defines an abstraction is its behavior, not
+how it's implemented -- this is the whole idea behind abstraction.
+
+The SICP version uses (LET ((CELL (LIST FALSE))) ...) because their
+TEST-AND-SET! tests and modifies a pair, not a variable, mainly so that
+it can be an ordinary procedure rather than a special form.  In this
+version there's no need to use a pair, although it wouldn't be wrong.
+
+The ugly (IF ((S (LAMBDA () ...))) ...) is necessary because of the way
+serializers work: they take a procedure as argument and return a protected
+version of that procedure, which must then actually be invoked.
+We didn't take off for minor failures in the notation, accepting even
+solutions in which the argument to the serializer was an expression to
+be evaluated atomically: (IF (S (IF IN-USE ...)) ...)  But we didn't
+accept solutions in which the serializer was given data, rather than
+activity, as its argument.  (More on that below.)
+
+The reason for the nested IFs is a little subtle; the recursive call
+to THE-MUTEX to retry the operation if it fails can't be inside the
+serialized procedure, because then it'll block waiting for S, which is
+held by this process itself -- in effect, setting up a deadlock within a
+single process.  But we didn't take off points for missing this subtlety.
+
+Some people used a single global serializer instead of a separate one
+for each mutex.  At first we considered this a serious error, because
+it means that two mutexes can't be acquired at the same time.  But then
+we realized that the serializer just protects the acquiring of the mutex,
+not the critical section that the mutex itself protects, so any mutex
+holds the serializer only for a very short time.  So we allowed a global
+serializer, except in papers that also missed the point in the previous
+paragraph.  If a mutex can get into a deadlock with itself, while holding
+a serializer needed by all the other mutexes, that's serious enough to
+lose a point.
+
+Some solutions never actually tested whether the mutex was in use before
+acquiring it.  Presumably those people thought that the serializer would
+protect the entire critical section, not just the acquiring, so that if
+an acquire operation could get into the serializer the mutex must be free.
+But that's not the case; the mutex returns to its caller once it has
+set its in-use flag, so the caller's critical section is *not* keeping the
+serializer busy.  Such solutions got at most one point.
+
+There were a few common zero-point solutions.  The strangest were the
+ones that called parallel-execute.  The job of a mutex is to protect
+critical sections against other processes, not to create other processes.
+
+Another zero-point solution had expressions such as (S CELL) that showed
+a lack of understanding of the key fact that a serializer protects
+activities, not data.  Similarly, no points for anything that included
+
+        (something . args)       ;; pfui
+
+which indicated mindless copying from the MAKE-SERIALIZER in the book
+without understanding.  Serializers accept as arguments procedures with
+any number of arguments, but any particular use of a serializer isn't
+going to be that complicated -- and none of these solutions found any
+use for the ARGS variable anyway.
+
+Scoring:
+3  OK
+2  at least an atomic test-and-set implementation
+1  no test-and-set, but some idea
+0  way off
+
+
+7.  Streams.
+
+The answer we expected was
+
+(define all-integers
+  (cons-stream 0 (interleave integers
+                             (scale-stream -1 integers))))
+
+The stream generated by this expression looks like this:
+
+        0, 1, -1, 2, -2, 3, -3, 4, -4, ...
+
+You had to understand two key points to get this solution:
+
+1.  You can't have all the integers in size order.  A stream must have
+a definite first element!  There's no smallest negative integer.  Some
+people tried to REVERSE the stream of integers; that's meaningless.
+
+2.  You can't APPEND-STREAMS two infinite streams.  This is explained
+in the handout you got in lecture.  Since the first stream will never
+finish, it's as if the second stream isn't there at all!  (A few people
+used MERGE, which works for two sorted, increasing streams, but not
+in a case like this where one stream is growing up and one growing down.)
+
+There were many other correct solutions, most of which essentially
+reinvented INTERLEAVE.  Special mention for the following beautiful
+solution (which the student unfortunately didn't get quite right):
+
+(define zeros-ones (cons-stream 0 (cons-stream 1 zeros-ones)))
+
+(define all-integers (subtract-streams zeros-ones
+                                       (cons-stream 0 all-integers)))
+
+where subtract-streams is like add-streams with the obvious difference.
+Work this out to convince yourself that it really works!
+
+Scoring:
+
+4  correct
+3  small mistakes, like leaving out zero
+2  append-stream etc.
+1  stream of streams, like (cons-stream negatives positives)
+0  not a stream at all
+
+We heard from several students who thought that the stream-of-streams
+solution was graded too harshly.  Here's the reasoning behind this
+grading policy:  Most errors come from uncertainty about how to
+implement an infinite stream successfully.  But this error indicates
+a misunderstanding about what the stream abstraction is for!  Think
+about the example of testing whether a number is prime.  We create
+a stream containing a range of numbers (range 2 14) and then we use
+that as an argument to FILTER:
+        (filter (lambda (number) (= (remainder 15 number) 0))
+                (range 2 14))
+
+If RANGE returned a stream of streams, then this FILTER wouldn't
+work, because the elements of the stream wouldn't be numbers!  So,
+even though the problem said "a stream containing all the integers"
+instead of "a stream containing all the integers, in which each
+element is a single integer," it's unreasonable to think that a
+stream whose elements aren't numbers would be acceptable.