Accelerated Intro to CS

Intro

This is a companion to this curriculum and goes through CS19 Accelerated Intro to CS in it's entirety. It's definitely the best undergrad intro course that exists.

BEGIN

We're going to start with Brown University's CS19 Accelerated Intro to CS. Note that this is not a public course, do not harass the professor or any TAs asking for solutions. You can ask the authors of the DCIC book we use anything about that book but it's a violation of all kinds of school rules for them to discuss this course or the solutions of a lab/assignment.

The professor teaching the lecture course has a unique lecture style and specializes in research driven curriculum. See his talk here about curriculum design as an engineering problem and the failure of knowledge transfer defined to mean a skill learned in one context is used successfully in another. "Even the students who have taken theory of computation (decidability) can't recognize the unsolvable problem (in another course)".

Materials available

  • the new book A Data-Centric Introduction to Computing
  • lectures & recitations on YouTube, torrent file if needed.
  • the Pyret online interpreter (CPO) to run code in your browser
  • the assignments
  • the labs at the bottom of this page (Big-O, Higher-Order Functions, Streams etc).
    • there is a similar intro course here also w/open lectures that uses the same book (doesn't cover the advanced chapter)

Many of the lectures only have about 20-30 mins of content, the rest is students being asked to work on something in the class (which you should try yourself).

Week 1 Placement

The accelerated course has a placement all students do in the summer for 6 weeks before being accepted for enrollment using the prof's other book HtDP.

If none of this makes sense, read Chapter 1 of the HtDP book it's a complete beginners guide. No really start at the beginning and read the entire book it is proven to teach you programming, they have papers about the success teaching it. I did the entire book using the lectures from edx but the book is a little harder than those lectures/assignments.

  • Task 1: Read the prologue and download Racket
  • Task 2: Add this to the top of your program: (require 2htdp/image)
  • Task 3: Create flags of the following countries: Vietnam, Chile, Suriname, Saint Lucia, Turkey

Look at the documentation for the image library. Let's walk through Chile planning first because you always plan out what you're going to do before you write it. We want an empty-scene 120 wide and 80 height, and a blue square about half the size of the scene placed in the left corner. Reading the documentation for the image library the coordinate system (x,y) places the center of the image, and the center of our 40 width square should be (20,20):

(require 2htdp/image)
(place-image (square 40 "solid" "blue")
             20 20
             (empty-scene 120 80))

Now add the star:

(require 2htdp/image)
(place-image (overlay (star 12 "solid" "white") (square 40 "solid" "blue"))
             20 20
             (empty-scene 120 80))

Now place a red rectangle underneath:

(require 2htdp/image)
(place-image (rectangle 120 40 "solid" "red")
             60 60
             (place-image (overlay (star 12 "solid" "white")(square 40 "solid" "blue"))
                          20 20
                          (empty-scene 120 80)))

Finish making the flags, there's a rotate function in the image library for the star on the Turkey flag. You can place-image on other images too it doesn't have to be a scene.

Placement 2

Writeup is here.

Task 1: Read Part 2 of HtDP 2/e, specifically chapters 8 and 9 through 9.1. Data definitions are defined in chapter 1 and Figure 56 describes what they want us to do both in the course and in this placement as it will help you to think through the problem.

Task 2: Implement the following functions:

score-by-length :: List-of-strings -> Number
Consumes a list of strings that represents words. Produces the sum of the lengths of these words. 
(Imagine you’re writing a game based on words, and this is a scoring function. We’ll return to this theme.)

overlay-all :: List-of-images -> Image
Consumes a list of images and overlays them, with earlier images in the list over later ones. 
Use a 10x10 solid white rectangle as the backdrop atop which all other images are overlaid.

Note: You may sometimes find that two images you expect to be equal are reported as not equal by Racket. 
This is because image equality is an imperfect art. 
Images that visually appear identical may in fact be very slightly different, causing a test to fail.

bar-graph :: List-of-numbers -> Image
The numbers in the list represent heights of bars in a bar graph 
(assume they have already been scaled and reflect the desired heights). 
Generate a bar graph where each bar is a black solid rectangle with a width of 10px, and successive bars are adjacent (use beside/align). 
Don’t have spaces between the bars.

Use a 1x1 solid white rectangle bar as the base case, so it will always be present as the right-most bar.
You can assume that all the numbers are non-negative.

is-in? :: Any List-of-any -> Boolean
Consumes a value and list of values and determines whether the former is anywhere in the latter: 
returns #true if it is and #false otherwise.
Use equal? for comparison.

words-to-sentence :: List-of-strings -> String
Consumes a list of strings that represent words. Concatenate these strings with a space between every pair of words.
To write this function, you may not use any built-in string functions other than string-append.

Let's try overlay-all, I took this template from the chapter on lists in HtDP:

(require 2htdp/image)
; List-of-images -> Image
; overlays aloi in reverse order to 10x10 white solid rectangle
(define (overlay-all aloi)
  (cond
    [(empty? aloi) ...]
    [else
     (... (first aloi) ...
          ... (overlay-all (rest aloi)) ...)]))
(check-expect (overlay-all '()) "There was no input!")
(check-expect (overlay-all (cons (circle 50 "solid" "blue") '()))
              (overlay (circle 50 "solid" "blue") (rectangle 10 10 "solid" "white")))                  
(check-expect (overlay-all (cons (circle 50 "solid" "blue")
                                 (cons (square 45 "solid" "red")
                                       (cons (square 40 "solid" "blue")
                                             (cons (star 12 "solid" "white")
                                                   '())))))
              (overlay (overlay (overlay (overlay (star 12 "solid" "white")
                                                  (square 40 "solid" "blue"))
                                         (square 45 "solid" "red"))
                                (circle 50 "solid" "blue"))
                       (rectangle 10 10 "solid" "white")))

Since our function signature is List -> Image meaning it's supposed to return an image I decided a fake error of 'No input' should be the result of giving it an empty list instead of returning the 10x10 rectangle. If you use a real (error "There was no input!") then the program throws an exception before running the other tests. Accumulators I don't think are in the reading so they want you to figure this out without using a helper/accumulator.

The check-expect tests tell us how to write the conditional. If the rest of the list is empty, link the (first aloi) to the rectangle. If we are recursing through a larger list overlaying the (first (rest aloi)) to the (first aloi) in a chain, once we get to the second last item, we want to overlay the last item and then return that image to be overlayed over everything else:

(require 2htdp/image)
; List-of-images -> Image
; overlays aloi in reverse order to 10x10 white solid rectangle
(define (overlay-all aloi)
  (cond
    [(empty? aloi) "There was no input!"]
    [(empty? (rest aloi)) (overlay (first aloi) (rectangle 10 10 "solid" "white"))]
    [(empty? (rest (rest aloi))) (overlay (first (rest aloi)) (first aloi))]
    [else
     (overlay (overlay (overlay-all (rest aloi)) (first aloi))
              (rectangle 10 10 "solid" "white"))]))
(check-expect (overlay-all '()) "There was no input!")
(check-expect (overlay-all (cons (circle 50 "solid" "blue") '()))
              (overlay (circle 50 "solid" "blue") (rectangle 10 10 "solid" "white")))                  
(check-expect (overlay-all (cons (circle 50 "solid" "blue")
                                 (cons (square 45 "solid" "red")
                                       (cons (square 40 "solid" "blue")
                                             (cons (star 12 "solid" "white")
                                                   '())))))
              (overlay (overlay (overlay (overlay (star 12 "solid" "white")
                                                  (square 40 "solid" "blue"))
                                         (square 45 "solid" "red"))
                                (circle 50 "solid" "blue"))
                       (rectangle 10 10 "solid" "white")))

The check-expects we wrote are examples, not tests. Here is a good writeup on what you should test for. Test for each possible answer in a conditional expression, test boundary points if you have inequalities, and/or expressions you test each case like first argument true, second is false etc. We'll later learn no matter how many tests you write you will only ever cover a very small surface of the entire program output so we will learn property-based testing where you use random inputs and test the properties of the results.

Recursion

If you've written loops in Python or similar language before you can for now consider these entire functions to be one big loop body. Every iteration just like a loop body you are doing one step at a time except instead of a block of code in a loop it's an entire function that loops with smaller inputs. Later we'll see you can return results while recursing and do mutual recursion so recursion is a little more than just a loop body but for now this is a good enough understanding. Really it's a fixed point that's mapped to itself.

Let's step length:

(define (len l)
  (cond
    [(empty? l) 0]
    [else (+ 1 (len (rest l)))]))
(check-expect (len '(1 2 3 )) 3)

The syntax '(1 2 3) is a list abbreviation you have to activate in DrRacket changing languages to 'BSL w/list abbreviations' now the syntax '() for the empty list makes sense.

  • (len '(1 2 3))
  • (+ 1 (len '(2 3))
  • (+ 1 (+ 1 (len '(3))))
  • (+ 1 (+ 1 (+ 1 (len '()))))
  • (+ 1 (+ 1 (+ 1 0)))
  • (+ 1 (+ 1 1))
  • (+ 1 2)

The evaluation is delayed until the recursion stops with the empty case returning 0, then (+ val val) has two inputs and can start evaluating.

Math

There is 2 math tasks.

Find the numerical value of: \(\sum_{n=1}^{100}(4n -3)\)

Write a program, this summation notation means an algorithm that sums (4n - 3) from n=1 to n=100 so n=2 is (4(1) - 3) + (4(2) - 3)

; Number -> Number
; sums (4n - 3) from 1 to n 
(define (summ n)
  (cond
    [(< n 1) 0]
    [else (+ (- (* 4 n ) 3) (summ (- n 1)))]))
(check-expect (summ 0) 0)
(check-expect (summ 1) 1)
(check-expect (summ 2) 6) 
(check-expect (summ 3) 15) 
(check-expect (summ 4) 28) 
(check-expect (summ 5) 45)
(summ 100)

To find a closed form solution, meaning something that doesn't need to iterate and can immediately return the value of n=100 try squaring the input, reduce the scalar 4n to 2n so you can estimate division if needed, and ignore the subtraction. Then compare the results of summ and closed-sum to figure out the closed form. This kind of experimenting with math is taught in the discrete math content here if you're interested.

; Number -> Number
; closed form attempt of (4n - 3) from 1 to n 
; try 2n^2 and compare result to (summ n)
(define (closed-sum n)
  (* 2 n n ))
(closed-sum 1) "differs by 1"
(closed-sum 2) "differs by 2"
(closed-sum 3) "differs by n"
(closed-sum 4)
(closed-sum 5)

Task2: Let f(x) = x + 1 and let g(x) = 2x − 4. Is there a number C with the property that for all x > C, we have g(x) > f(x)? Note it says 'is there a number' not what is the lowest number.

Placement 3

Third placement. Read Part 2 of HtDP 2/e, specifically chapter 10.1

In Placement 2 you defined the function is-in?. This is built into Racket with the name member. You may use member from now on. Note that member uses equal? to perform its comparisons.

We will also be using characters, which are the atomic elements of strings. Characters are written with a #\ prefix: e.g., #\c, #\s. You can turn strings into lists of characters and vice versa using string->list and vice versa with list->string.

From now on, for the rest of the placement and the course: in every assignment, you can use any functions defined in that and any previous assignment, as well as libraries permitted in that and previous assignments, unless indicated otherwise—but no other. There may well be built-in functions that do exactly what the assignment asks for, but unless the assignment is expressly about your ability to search, we want you to demonstrate to us that you can write it with the resources we’ve asked you to use.

Some functions in this assignment would benefit from creating a helper function to handle a complex sub-task.

Tasks to write:

elim-contains-char :: Char List-of-strings -> List-of-strings
Consumes a list of strings and produces a list of the same words, in the same order, excluding those strings that contain the given character.

valid-words :: List-of-strings List-of-chars -> List-of-strings
Consumes a list of words (represented as strings) and a list of letters (represented as characters). 
Produces the list of same words, in the same order, that contain only those letters.
(Imagine a game where you have to assemble words with just the letters you have.)

For this assignment, ignore multiplicity: i.e., a word may contain more instances of a letter than the number of times 
the letter was present in the second parameter. Also, letters should be case-sensitive (e.g., #\A does not appear in "cat").

unique :: List-of-any -> List-of-any
Given a list of values, produces a list of the same values in the same order excluding duplicates.
If a value is present more than once, its first instance stays and the remainder are discarded. Use equal? for comparison.

l33t :: List-of-strings -> List-of-strings
Consumes a list of words (represented as strings) and produces a list of the same words, in the same order, 
but where some of the letters have been replaced by characters that stand for numbers. Specifically, it turns #\A and #\a into #\4, 
#\E and #\e into #\3, #\I and #\i into #\1, and #\O and #\o into #\0.

Note: The first letter of the function name we are asking for is the letter ‘l’ (Lima), not the number ‘1’ (One). 
It’s pronounced “leet” and is favored in basements worldwide. If you spell it incorrectly, the grading software will not give you any credit!

strip-vowels :: List-of-strings -> List-of-strings
Consumes a list of words (represented as strings) and produces the same list of words, except with each vowel 
(#\a, #\e, #\i, #\o, or #\u, or their upper-case equivalents) removed. 
(If a word consists of all vowels, it reduces to the empty string but is not removed entirely.)

Note: Be careful. There’s a simple, clean decomposition of tasks in this problem, but if you rush you may end up with a mess.

singles :: List-of-numbers -> List-of-numbers
Consumes a list of numbers and produces the same list, but where any “run” of numbers (i.e., consecutive numbers that are identical) is replaced by a single occurrence of that number. For instance, the list (cons 1 (cons 2 (cons 2 (cons 4 empty)))) would be transformed into (cons 1 (cons 2 (cons 4 empty))).

Let's try strip-vowels. It probably needs a helper function:

; List-of-char -> List-of-char
; strips vowels from aloc
(define (strip-vowels-helper aloc)
  (cond
    [(empty? aloc) ... ]
    [else
     (... (first aloc) ...
          ... (strip-vowels-helper (rest aloc)))]))
(check-expect (strip-vowels-helper '()) '())
(check-expect (strip-vowels-helper (string->list "")) '())
(check-expect (strip-vowels-helper (string->list "aeiou")) '())
(check-expect (strip-vowels-helper (string->list "AEIOU")) '())
(check-expect (strip-vowels-helper (string->list "Test")) '(#\T #\s #\t))

; List-of-strings -> List-of-strings
; strips vowels from strings in alos
(define (strip-vowels alos)
  (cond
    [(empty? alos) ... ]
    [else
     (... (first alos) ...
          ... (strip-vowels (rest alos)))]))
(check-expect (strip-vowels '()) '())
(check-expect (strip-vowels '("aeiou")) '(""))
(check-expect (strip-vowels '("Hello")) '("Hll"))
(check-expect (strip-vowels '("AEIOUy")) '("y"))

Reminder I changed the language in DrRacket to 'BSD with list abbreviations' so wouldn't have to manually cons out a bunch of lists. The examples we write tell us what to do: loop through the input and call (strip-vowels-helper((string->list (first alos))) and test when it returns if it's empty. If so cons "" if not cons the result using list->string.

Placement 4

The task: read from Part 3 of HtDP 2/e, specifically chapters:

  • 14.1
  • 14.2
  • 14.4
  • 15.1
  • 15.4
  • 16.1
  • 16.2
  • 16.5

Then change language in DrRacket to 'Student with Lambda' and rewrite the functions we did in placement 3 with only: map filter foldl foldr andmap ormap. The task notes don't be surprised if you find some of the functions challenging. There is a planning component of moving program blocks around that still is open access but since we aren't actual students submitting anything I trust you can plan the programs yourself on a piece of paper. The more you plan this out with examples/tests the easier it is to code.

Let's re-do strip-vowels from last placement:

; Char -> Boolean
; tests if c is a vowel 
(define (is-vowel? c)
  (ormap (lambda (x) (member? x '(#\a #\e #\i #\o #\u #\A #\E #\I #\O #\U))) (cons c '())))

; List-of-chars -> List-of-chars
; filters l for vowels
(define (filtered l)
  (filter (lambda (x) (if (equal? (is-vowel? x) #true)
                          #false
                          #true)) l))

; List-of-Strings -> List-of-Strings
; removes vowels from strings in alos
(define (strip-vowels alos)
  (map (lambda(x) (if (equal? (filtered (string->list x)) '())
                      ""
                      (list->string (filtered (string->list x))))) alos))                   
                    
(check-expect (strip-vowels '()) '())
(check-expect (strip-vowels '("aeiou")) '(""))
(check-expect (strip-vowels '("Hello")) '("Hll"))
(check-expect (strip-vowels '("AEIOUy")) '("y"))

Filter consumes a function (is-vowel?) and a list, applying the function to each element of the list and returning a new list for anything true so notice I have the if-statement return false if a vowel is found so filter will remove it.

We'll see all this again in the actual course (in a different programming language) if you didn't fully understand the placement tasks, this material takes repeated practice to learn you never really get it right away.

Week 2 Accelerated Intro to CS

Watching the cs19 lecture titled Wed 9-5-18 or click here.

Go to code.pyret.org (referred to as CPO) and click open editor as you follow along the lecture, he teaches you the basics of the online IDE. Anything you don't understand here is in the reading.

We don't have access to Examplar but it gives you a sense how many quality examples he expects. The Fermi problems prof K talks about are all over the internet. Looking at example interview questions on glassdoor for some well known wall street outfits, one of their Fermi questions is 'estimate the number of books in your city' to see how skilled you are at approximation.

DCIC book

There is an optional recorded recitation for the beginning chapters here.

Read through the book while you have CPO open (https://code.pyret.org). If you make a mistake like accidentally deleting all your code Ctrl-Z will undo on most browsers I tested it on.

I trust you can figure the book out yourself and will use it as your research project for trying to do the assignments. You don't have to read it linearly as many of the lectures will cover the material in different order.

Assignment 1

This assignment DocDiff was released the first lecture. Watch this lecture starting @33:11 if you are curious to see what these document vectors mean in 3D space. Watch this recitation on lists and recursion in Pyret if you want but through doing the assignments you'll figure it out. Refer to the DCIC book chapter on lists.

Reminder you use https://code.pyret.org/ for your implementation, and can log in with some throwaway google account if you want to save your programs.

The dot product algebraic definition, look at the example for [1,3,-5] x [4,-2,-1]. Each index in the first list is multiplied by it's corresponding index in the second list, then those results are added together. To read the sigma notation, a*b = the sum of \(a_ib_i\) starting at index 1 (first element of the list) up to the length of the list (n). If a = \([1_{1} ,3_{2}, -5_{3}]\) and b = \([4_{1}, -2_{2}, -1_{3}]\):

\(a*b = \sum_{i=1}^3 a_ib_i\) = (\(1_{a1} * 4_{b1})+(3_{a2} * -2_{b2})+(-5_{a3} * -1_{b3})\)

Let's go through the design recipe we did in placement. First step, write a function signature by analyzing the values the program is working with, both input and output. We are given the signature, the input is two lists of strings, who's case we ignore, and the output will be a value with square roots involved (irrational numbers), so we are dealing with Roughnums or decimals that can't be compared to each other using normal 'is' check blocks.

fun overlap(doc1 :: List<String>, doc2 :: List<String>) -> Number:

Next step understand the data definition. These are lists of strings so cases on empty or link(f,r). To vectorize both documents, we need a distinct combined list of both List<String> inputs, that is converted to lower case, that is sorted, to represent the word freq count of each document.

Example input to overap:
doc1 = [list: "a", "B", "c"]
doc2 = [list: "d", "d", "D", "b"]

Distinct/lowercase and sorted combination of doc1/doc2
doc1 + doc2 [list: "a", "b", "c", "d"]

Total words:[list: "a", "b", "c", "d"]
doc1-vec =  [list:  1,   1,   1,   0]
doc2-vec =  [list:  0,   1,   0,   3]

Now we write some examples for the main function. It is returning an 'overlap' between 0 and 1, so this is some probability distribution where two documents of length 3 that have 2 same words is 2/3, and if all 3 are are the same then 3/3 or 1. No similar words is 0/3 or 0.

fun overlap(doc1 :: List<String>, doc2 :: List<String>) -> Number:
  doc: ""
  lower-doc1 = map(string-to-lower, doc1)
  
where:
 # these examples taken from the Examplar paper
  overlap([list: "welcome", "to", "Walmart"], 
    [list: "WELCOME", "To", "walmart"]) is-roughly 3/3
  overlap([list: "1", "!", "A", "?", "b"], 
    [list: "1", "A", "b"]) is-roughly 3/5
  overlap([list: "alakazam", "abra"],
    [list: "abra", "kadabra", "alakazam", "abra"]) is-roughly 2/4
  overlap([list: "a", "b"], [list: "c"]) is 0/3

  # epsilon test for roughnums
  epsilon = 0.001
  a = [list: "alakazam", "abra"]
  b = [list: "abra", "kadabra", "alakazam", "abra"]

  num-abs(overlap(a, b) - 2/4) <= epsilon is true  
end

Look in the documentation for binary operators you can use, and built-in libraries. The math equation we just have to translate it to a program using built-ins. This is how I interpreted it:

(dot-product(vec1, vec2)) / num-max(
    num-sqr(num-sqrt(dot-product(vec1, vec1))), 
    num-sqr(num-sqrt(dot-product(vec2, vec2))))

Click to see my solution

This is just one solution, not the 'best' solution, you can design your programs however you want.

include lists

fun dot-product(vec-a :: List<Number>, vec-b :: List<Number>)-> Number:
  doc: "Returns the dot-product of vec-a and vec-b"
  if vec-a.length() == vec-b.length():
    fold2(lam(acc, elt1, elt2): acc + (elt1 * elt2) end, 0, vec-a, vec-b)
  else:
    raise("Vectors are not the same dimensions")
  end
where:
  dot-product([list: 0, 2, 3], [list: 3, 0, 0]) is 0
  dot-product(empty, [list: 1]) raises "Vectors are not the same dimensions"
end

fun make-vec(both-docs :: List<String>, d :: List<String>)-> List<Number>:
  doc: "Creates a vector of d using both-docs"
  cases (List) both-docs: 
    | empty => empty
    | link(f, r) => 
      if d.member(f):
        link(d.filter(lam(x): string-equal(f, x) end).length(),
          make-vec(r, d.filter(lam(x): not(x == f) end)))
      else:
        link(0, make-vec(r, d))
      end
  end
where:
  make-vec([list: "a", "b", "c"], [list: "d"]) is [list: 0, 0, 0]
  # Property-based test where 2 similar words should be [list: 0, 2, 0]
  make-vec([list: "a", "b", "c"], [list: "b", "b"])
    .foldl(lam(elt, acc): acc + elt end, 0) is 2
  # Property-based test where all similar words should be length of vec
  make-vec([list: "a", "b", "c"], [list: "a", "a", "a"]).length() is 3

end

fun overlap(doc1 :: List<String>, doc2 :: List<String>)-> Number:
  doc: "Computes the overlap between doc1 and doc2"

  if (doc1 == empty) or (doc2 == empty):
    raise("Empty input")
  else:
    combined = distinct(doc1.append(doc2).map(string-to-lower).sort())
    vec1 = make-vec(combined, doc1.map(string-to-lower))
    vec2 = make-vec(combined, doc2.map(string-to-lower))
    # normalized 
    (dot-product(vec1, vec2)) / num-max(
      num-sqr(num-sqrt(dot-product(vec1, vec1))), 
      num-sqr(num-sqrt(dot-product(vec2, vec2))))
  end
where:
  overlap(empty, empty) raises "Empty input"
  # these examples taken from the Examplar paper
  overlap([list: "welcome", "to", "Walmart"], 
    [list: "WELCOME", "To", "walmart"]) is-roughly 3/3
  overlap([list: "1", "!", "A", "?", "b"], 
    [list: "1", "A", "b"]) is-roughly 3/5
  overlap([list: "alakazam", "abra"],
    [list: "abra", "kadabra", "alakazam", "abra"]) is-roughly 2/4
  overlap([list: "a", "b"], [list: "c"]) is 0/3

  # epsilon test for roughnums
  epsilon = 0.00001
  a = [list: "alakazam", "abra"]
  b = [list: "abra", "kadabra", "alakazam", "abra"]

  num-abs(overlap(a, b) - 2/4) <= epsilon is true  
end

Lecture 2 Rainfall Problem

Watch the Mon9/10/18 second lecture. The first task try it in Pyret yourself and see if you made any mistakes as later in the lecture he'll show all the errors students typically make doing this. This optional recitation video talks about good examples and the program directory meaning what is in scope. The end introduces table shaped data ie: databases and vectors.

Lab: Higher-Order Functions

Let's do the lab on functions as data, and write our own map, filter and fold. The first example enter some built-in functions like num-sqrt/num-sqr:

fun fun-plus-one(num :: Number, func :: (Number -> Number)) -> Number:
  func(num) + 1
end

>>fun-plus-one(16, num-sqrt)
>>5
>>fun-plus-one(3, num-sqr)
>>10

Map

We are asked to implement goldilocks using map, write your own predicate function, look in the documentation for else if/if statements:

fun goldilocks(n :: Number) -> String:
  if ...
 end
end

check:
  map(goldilocks, [list: 131, 77, 68]) is [list: "too hot", "just right", "too cold"]
end

I used the documentation examples for map as tests. I used 'type parameters' <A,B> since map function can transform the type (this is in the DCIC book). The variables A and B are like math variables a + b = 0 where a and b can either both be 0 and thus the same or they can be different with a = 1 and b = -1 so (A -> B) can be a function with same or different types. They just have to be consistent with the return List<B> or Pyret gives a type error if you run this with type checking on (click the down arrow on the run button, change to Type-check and Run).

fun my-map<A,B>(func :: (A -> B), l :: List<A>) -> List<B>:
  cases (List) l:
    | empty => empty
    | link(f, r) => 
      ...
  end
where:
  my-map(num-to-string, [list: 1, 2]) is [list: "1", "2"]
  my-map(lam(x): x + 1 end, [list: 1, 2]) is [list: 2, 3]
end

Filter

Write your own filter

fun my-filter<T>(func :: ( T -> Boolean), l :: List<T>)-> List<T>:
  cases (List) l:
    | empty => empty
    | link(f, r) =>
      ...   
  end
end
check:
  # these are all tests from the Pyret docs for .filter
  fun length-is-one(s :: String) -> Boolean:
    string-length(s) == 1
  end
  my-filter(length-is-one, [list: "ab", "a", "", "c"]) is [list: "a", "c"]
  my-filter(is-link, [list: empty, link(1, empty), empty]) is [list: link(1, empty)]
  my-filter(lam(x): x > 0 end, [list: 0, 1, -1]) is [list: 1]
end

Fold

Write your own fold:

fun my-fold<A,B>(func :: (B, A -> B), acc :: B, l :: List<A>) -> B:
  cases (List) l:
    | empty => acc
    | link(f, r) =>
          ...
  end
end
check:
  my-fold((lam(acc, elt): acc + elt end), 0, [list: 3, 2, 1]) is 6

  fun combine(acc, elt) -> String:
    tostring(elt) + " - " + acc
  end

  my-fold(combine, "END", [list: 3, 2, 1]) is "1 - 2 - 3 - END"
  my-fold(combine, "END", empty) is "END"
end

Map2

Write map2:

# link(ff, rr) and link(f, r) can be called anything:

fun my-map2<A,B>(func :: (A, A -> B), list1 :: List<A>, list2 :: List<A>) -> List<B>:
  cases (List) list1:
    | empty => empty
    | link(f, r) =>
      cases (List) list2:
        | empty => empty
        | link(ff, rr) =>
          ...
      end
  end
end
check:
  my-map2(string-append, [list: "mis", "mal"], [list: "fortune", "practice"])
    is [list: "misfortune", "malpractice"]
  my-map2(_ + _, [list: "mis", "mal"], [list: "fortune", "practice"])
    is [list: "misfortune", "malpractice"]
  my-map2(string-append, [list: "mis", "mal"], [list: "fortune"])
    is [list: "misfortune"]
  my-map2(string-append, [list: "mis", "mal"], empty)
    is empty
  # test type signature
  my-map2(lam(x, y): if (x > 0) and (y > 0): true else: false end end, [list: 0, 1], [list: 2, 3]) 
    is [list: false, true] 
end

The last task, best-price, look on youtube what a basic demand function is:

fun demand(price-increase :: Number) -> Number:
 doc: "1000 is approx demand of product at a fixed price"
  1000 - (60 * price-increase)
end

This means as the price increases, the quantity demanded will decrease by 60 units times the price increase.

Assignment 2

If you watched lecture 2 the students talked about starting Nile already so let's do it too. Here is the writeup. It's collaborative filtering but we have to only use lists and rewrite built-in we use ourselves. The prof considers rewriting all the built-ins good drill to learn this material so you have to use string-to-code-points and write your own member, append, etc., using cases on list link/empty.

As usual try some examples:

data Recommendation:
  | recommendation(count :: Number, names :: List<String>)
end

data File:
  | file(name :: String, content :: String)
end

fun recommend(title :: String, book-records :: List<File>) -> Recommendation:
  ...
end

check:
  file-1 = file("", "Crime and Punishment\nHeaps are Lame\nLord of the Flies")
  file-2 = file("", "Crime and Punishment\nRandom Book\nLord of the Flies")
  file-3 = file("", "Test Book\nHeaps are Lame\nRandom Book")

  input = [list: file-1, file-2, file-3]

  recommend("Crime and Punishment", input) is recommendation(2, [list: "Lord of the Flies"])
  recommend("Test Book", input) is recommendation(1, [list: "Heaps are Lame", "Random Book"])
  recommend("No Recommendation", input) is recommendation(0, empty)
end

This assignment is a beast and can quickly grow into 100+ lines of code but that's the point of the assignment, it forces you to dig into Pyret docs and spend time practicing since programming is a learn by doing skill. There's a few built-ins that can help you write this like Spies which is print-style debugging to show the value of something.

In later versions of the course they changed popular-pairs to a BookPair datatype:

recommendation(2, [list: pair("a", "b"), pair("c", "d")]) 

If you write recommend generic enough splitting all the program logic into little reusable functions then popular-pairs likely won't require any new functions.

Click to see my solution

I wrote this on a phone during a lunch break. I used + to concat two lists together, which I can't find anywhere in the documentation but I rewrote it myself anyway. I didn't rewrite length/distinct/member since I've already done so many times, if you haven't try it, this assignment is all about practice. All you need to do is get something working, then you learned enough about the problem you can trash all the code and rewrite it.

data Recommendation:
  | recommendation(count :: Number, names :: List<String>)
end

data File:
  | file(name :: String, content :: String)
end

fun cat<T>(l1 :: List<T>, l2 :: List<T>)-> List<T>:
  doc: "Rewrite of the list concat operator"
  if l1 == empty:
    l2
  else if l2 == empty:
    l1
  else:
    foldr(lam(acc, elt): 
        # if we find an embedded list, fold it using the acc from l1
        if is-link(elt):
          foldr(lam(ac, x): link(x, ac) end, acc, elt)
        else:
          link(elt, acc)
        end
      end, empty, link(l1, l2))
  end
where:
  cat([list: 1, 2], [list: 3, 4]) is [list: 1, 2, 3, 4]
  cat([list: 1], [list: ]) is [list: 1]
  cat([list: ], [list: 1]) is [list: 1]
end

fun data-clean(s :: String)-> List<String>:
  doc: "Removes newline codepoint from l"

  fun helper(lon :: List<Number>, acc :: String)-> List<String>:
    cases (List) lon:
      |empty => link(acc, empty)
      |link(f, r) =>
        # string-to-code-point("\n") == 10
        if f == 10:
          link(acc, helper(r, ""))
        else:
          helper(r, acc + string-from-code-point(f))
        end
    end
  end

  if (s == ""):
    empty
  else:
    helper(string-to-code-points(s), "")
  end   
where:
  data-clean("C\na") is [list: "C", "a"]
  data-clean("") is empty
end

fun find-recs(f :: File, t :: String)-> List<String>:
  doc: "List of titles from f containing t" 
  book-list = data-clean(f.content)
  # if t is in data-clean(file) return list without t
  if book-list.filter(lam(elt): elt == t end).length() > 0:
    book-list.filter(lam(elt): not(elt == t) end)
  else:
    empty
  end
end

fun combine-recs(lof :: List<File>, t :: String)-> List<String>:
  doc: "Combines all books (including dupes) from all files containing t"
  cases (List) lof:
    | empty => empty
    | link(f, r) => find-recs(f, t) + combine-recs(r, t)
  end
end

fun make-recs(los1 :: List<String>, los2 :: List<String>)->List<Recommendation>:
  doc:"List of recommendations from los1"
  cases (List) los1:
    | empty => empty
    | link(f, r) =>
      # filter r to prevent dupes after building recommendation for f
      link(recommendation(los2.filter(lam(x): f == x end).length(), link(f, empty)), 
        make-recs(r.filter(lam(elt): not(f == elt) end), los2))
  end
end

fun find-max(lor :: List<Recommendation>, acc :: Recommendation)-> Recommendation:
  doc:"The largest recommendation in lor"
  cases (List) lor:
    | empty => acc
    | link(f, r) =>
      if f.count >= acc.count:
        # if f is biggest it's the new acc
        find-max(r, f)
      else:
        find-max(r, acc)
      end
  end
end

fun recommend(title :: String, book-records :: List<File>)-> Recommendation:
  doc: "Recommends most often bought books with title"

  all-books = combine-recs(book-records, title)
  recs = make-recs(all-books, all-books)
  max = find-max(recs, recommendation(0, [list: ]))
  if max.count > 0:
    # filter the list of recs by count, link names, sort result
    recommendation(max.count, recs.filter(lam(x): x.count == max.count end)
        .foldl(lam(elt, acc): elt.names + acc end, empty).sort())
  else:
    max
  end
where:
  file-1 = file("", "Crime and Punishment\nHeaps are Lame\nLord of the Flies")
  file-2 = file("", "Crime and Punishment\nRandom Book\nLord of the Flies")
  file-3 = file("", "Test Book\nHeaps are Lame\nRandom Book")
  input = [list: file-1, file-2, file-3]

  recommend("No Recommendation", input) is recommendation(0, empty)
  recommend("Crime and Punishment", input) is recommendation(2, [list: "Lord of the Flies"])
  recommend("Test Book", input) is recommendation(1, [list: "Heaps are Lame", "Random Book"])
  recommend("No Recommendation", input) is recommendation(0, empty)

  file-4 = file("", "a\nb\nc\nd")
  file-5 = file("", "d\na")
  input2 = [list: file-4, file-5]
  recommend("d", input2) is recommendation(2, [list: "a"])
end

data BookPair:
  | pair(book1 :: String, book2 :: String)
end

fun popular-pairs(records :: List<File>)-> Recommendation<BookPair>:
  doc: "Find the most often paired titles in record"

  # fold records and get a distinct list of all titles
  cleaned = distinct(records.foldl(lam(x, acc): acc + data-clean(x.content) end, empty))
  # map over titles and get recommendations for each 
  all-pairs = cleaned.map(lam(x): recommendation(recommend(x, records).count, 
      recommend(x, records).names.map(lam(elt): pair(elt, x) end)) end)
  # find the max recommendation and filter all-pairs 
  max-pair = find-max(all-pairs, recommendation(0, empty))
  result = all-pairs.filter(lam(x): x.count == max-pair.count end)
  # create recommendation with list of duplicate pairs 
  dupe-pairs = result.foldl(lam(elt, acc): recommendation(max-pair.count, acc.names + elt.names) end, recommendation(0, empty))
  # return recommendation(count, distinct list of pairs)
  recommendation(max-pair.count, 
    dupe-pairs.names.foldl(lam(x, acc): 
      if acc.member(pair(x.book2, x.book1)): acc + empty else: acc + [list: x] end end, empty))
where:

  a = file("", "C\nH\nL")
  b = file("", "C\nR\nL")
  c = file("", "T\nH\nR")

  popular-pairs([list: a]) is recommendation(1, [list: pair("H", "C"), pair("L", "C"), pair("L", "H")])
  popular-pairs([list: a, b, c]) is recommendation(2, [list: pair("L", "C")])
  popular-pairs(empty) is recommendation(0, empty)
end

Week 3 Accelerated Into to CS

We're watching Wed9/12/18 lecture on sorting.

This is the code on the board which you should have typed into CPO yourself:

fun insert(n :: Number, l :: List<Number>) -> List<Number>:
  cases (List) l:
    | empty => [list: n]
    | link(f, r) => if n <= f:
        link(n, l)
      else:
        link(f, insert(n, r))
      end
  end
where:
  # student given examples:
  insert(2,  [list: 3,4,5])   is [list: 2,3,4,5]
  insert(1,  [list: 0,1,1,2]) is [list: 0,1,1,1,2]
  insert(-1, [list: -2,0,3])  is [list: -2,-1,0,3]
  insert(3,  [list: ])        is [list: 3]
  insert(4,  [list: 1,2,3])   is [list: 1,2,3,4]
end

fun sort(l :: List<Number>) -> List<Number>:
  cases (List) l:
    | empty => empty
    | link(f, r) => insert(f, sort(r))
  end
where:
  sort([list: 3,2,1])        is [list: 1,2,3]
  sort([list: 1,2,3])        is [list: 1,2,3] 
  sort([list: 4,1,-1])       is [list: -1, 1, 4]
  sort([list: 0.1, 0, 0.11]) is [list: 0, 0.1, 0.11]
end

Let's step the evaluation by hand with [list: 0, 1, 3, 2] being input to sort(). We call insert(0, sort(r)) but computation is delayed as we have to wait for the calls to sort(r) to complete since everytime we enter the cases with a non empty list to sort, it calls itself with the rest of the list. We end up with this delayed computation:

  • insert(0, (insert(1, (insert(3, (insert(2, empty)))))))

The returns to sort():

  • insert(0,(insert(1,(insert(3, [list: 2])))))
  • insert(0,(insert(1, [list: 2, 3])))
  • insert(0, [list: 1, 2, 3])

Everytime a list is returned, the next call to insert can happen because it is complete with insert(n, list).

Assignment 3

This assignment we're building a testing oracle.

First function is generate-input(n :: Number) -> List<Person>, it consumes a number, and produces a list of length n of type Person. Since this function is going to generate random person.name strings we can't directly match the output in a test case, but we can still confirm it has the correct properties of [list: person(name, number)]. You can recurse on n (if n > 0 link(name, generate-input(n - 1)):

data Person:
  | person(name :: String, age :: Number)
end

fun generate-input(n :: Number) -> List<Person>:
  doc:"Generate random n size list of person(name, age)"
  ...
where:
  # confirm size
  L.length(generate-input(5)) is L.length([list: 1, 2, 3, 4, 5])
  map(lam(x): (string-length(x.name) > 0) is true end, generate-input(5))  

  # make sure the age > 0
  map(lam(x): x.age > 0 is true end, generate-input(5))
end 

This assignment doesn't allow using built-ins outside of the higher order functions and sort() or sort-by() though I used some other list built-ins in the test cases since we already rewrote them in the Nile assignment. The is-valid() implementation where you can use map2 over both lists, the 'correct sorter' list and the other. The fold over the results is inefficient as a cases on List can break on the first instance of false instead of continuing through the entire linear list:

fun is-valid(test-input :: List<Person>, correct-input :: List<Person>) -> Boolean:
  doc: "Consumes test-input and compares to correct-input a correct-sorter() list"

  fun length-test(test-input-a :: List<Person>, correct-input-a :: List<Person>) -> Boolean:
    doc: "Test if both lists are the same length"
    (L.length(test-input-a)) == (L.length(correct-input-a))
  end

  fun name-test(test-input-b :: List<Person>, correct-input-b :: List<Person>) -> Boolean:
    doc: "Test if any names have been altered"
    correct-sorted-names = correct-sorter(test-input-b)
    names = map2(lam(n1 :: Person, n2 :: Person): (n1.name == n2.name) end, correct-sorted-names, correct-input-b)
    L.foldl(lam(a :: Boolean, b :: Boolean): a and b end, true, names)
  end

... many more tests 

  # return the results
if (test-input == empty) or (correct-input == empty):
  raise("Missing input")
else if length-test(test-input, correct-input)
  and
  name-test(test-input, correct-input)
  and
  age-test(test-input, correct-input):
... many more tests
  true
else:
  false

For testing think about all the ways you could have multiple valid outputs like people with duplicate names.

Check your work against the official solution

Write your solution first, then read this pdf. He writes about how this course encourages property-based testing meaning test for properties that are always true under random inputs instead of testing specific values and there is a writeup for this sortacle assignment what they graded for and exactly what would fail their tests.

The relation is between a list and it's sorted self.

  • the size of list and the sorted list must be the same
  • the elements of list and sorted list must be the same (no new, or missing)
  • the elements of the sorted list must be ordered correctly

We have to create is-valid a function when applied to list a and correct-sorted list b returns the property (boolean) that the size of a and b must be the same, the elements of a and b are the same etc.

They give an example of [a, a] and [a, a, a] both having same elements and sorted but not the same size. New elements being introduced is talked about in 5.1 What have we learned:

"Students suffered from a “directionality” problem: students’ property-based testing failed to notice new elements introduced in the output that were not in the input. This is reasonable from an operational perspective: while a buggy implementation could easily duplicate an input element in the output, it would be a rare implementation whose output contained entirely new elements that were not in the input. In a specification setting, however, this is a dangerous detail to overlook, because it would (for instance) give a synthesis algorithm freedom to generate an implementation that the specifier almost certainly did not want. Therefore, while this phenomenon is not widespread in this student population on these problems, its occurrence is still noteworthy, and the transition from property-based testing to full-on specification would need to address this.

Synthesis here means a code generating tool, for example coq theorem prover you can use it to generate code and of course something like chatgpt generates code. The techniques used in this paper are taught in their Logic for Systems course and the course notes are detailed. They used Alloy in the paper, Daniel Jackson's book Software Abstractions is the documentation for it where you use a modeling language as a 'lightweight verification' tool proving all the logic of your design before you write it.

Lecture 4 Big-O

Watching the Fri 9/14/18 lecture on performance here.

@23:33 he is arbitrarily counting any operation as a single unit of time except the recusive loop, we'll see in a few minutes that that none of these small constants really matter in worst-case analysis. Using length as an example: for the empty case (when the algorithm terminates) there's 4 steps, for every other case there is 5 steps. Length loops k times or the size of the input. So we have 5k + 4 our running time function or [k -> 5k + 4].

@39:43 he draws [k -> 5k + 4] is roughly [k -> 5k] or [k -> k] to suppress unimportant details because as he elaborates once k (the input) gets huge it's growth is really just k or linear.

@46:00 there exists some constant c that for all values of k: f(k) \(\le\) c * g(k). The f(k) function on the left is a model of your running time 5k + 4, the cg(k) function on the right is an arbitrary function you choose from a list of existing math functions that closely bounds f(k). The actual constant value c that scales or multiplies g(k) doesn't matter just that it exists.

Example:

  • f(n) = 5n + 4 and cg(n) = n
  • What if c = 10 and we have 100 inputs
    • 5(100) + 4 \(\le\) 10(100)
    • Right side cg(n) already has bounded f(n)
    • Our running time function is in O(g(n))

DCIC Predicting Growth

This chapter is one of the best in the book because it gives you everything for basic big-O analysis, but the course/book assumes you will go on to an algorithm analysis book later so can't cover everything.

Reading DCIC. The arbitrary counting he did in the lecture giving a unit of time to operations is a little different in the book where he adds some more constants to certain operations but of course none of these small constants matter. The tabular method only works for programs that are purely conditionals like our cases on list programs. The 'question' or predicate is the left hand part of the => in cases on List, 'the answer' is the returned result on the right hand side of =>.

The exercise in the tabular method asks why is it justified to ignore the recursive call because really the entire program is the body of a loop, so whatever is in the body you multiply it by how many times you loop/recurse, which is k times for k input.

The exercise in 13.8 for big-theta function equality, big-theta is defined as: cg(x) \(\le\) f(x) \(\le\) dg(x) where both c and d are arbitrary constants. This means if there exists a constant factor where g(x) is less or equal to f(x), and a constant factor where g(x) is bigger or equal to f(x), then they grow at the same speed or are equal. Reflexivity means something equals itself and using our above inequality where f(x) can be less than, greater or equal to g(x) then f(x) = g(x) so we can substitute g(x) for f(x) and f(x) = f(x).

In 13.9 O(max(F,G)) is also how you do analysis on sequential programs in imperative programming you analyze blocks of code and take whatever the max block cost is. If there's loops then as we learned in the lecture you simply multiply the amount of times you loop by whatever operations are in the body and if it's recursive code the entire function is the loop body. If the loop calls another loop, then you multiply both their big-O together.

13.10 Solving Recurrences c0 or c1 is the notation for the constants in the base case or empty case. Try to make your own geometric interpretations for all the recurrences like what's in the book for k2, for example T(n/2) + c if you input 8 has height of 3 or log(8) height (all the logs are base 2).

All the recurrences in the book have an accompanying video somewhere on YouTube, such as this guy's playlist. I'm going to do it completely different exploiting asymptotics by filling in some values for k to find the big-O not the recurrence closed form solution which is different. Here I am doing what you should do which is just entering in values and seeing what happens:

  • T(k) = T(k - 1) + c
    • T(1) = T(1 - 1) or T(0) + c
    • T(2) = T(2 - 1) or T(1) + c
    • T(3) = T(3 - 1) or T(2) + c

T(3) total is c + c + c + c0 or kc or 3c since k=3 or O(k)

  • T(k) = T(k - 1) + k
    • T(1) = T(0) + k
    • T(2) = T(1) + k
    • T(3) = T(2) + k

T(3) total is k + k + k + c0 or kk or 3*3 since T(k=3) so O(k2)

  • T(k) = T(k/2) + c
    • T(2) = c1 + c
    • T(4) = T(2) + c or 2c
    • T(8) = T(4) + c or 3c

T(4) is c(log 4) and T(8) is c(log 8) or O(log k)

  • T(k) = T(k/2) + k

Same as above except it's T(4) = k(log 4) or 2k which is of course constant * k or O(k)

  • T(k) = 2T(k/2) + k
    • T(2) = 2T(1) + k or k
    • T(4) = 2T(2) + k or 3k
    • T(8) = 2T(4) + k or 7k
      • k(2log 8 - 1) or since k=8 k(2log k - 1)
      • O(k * log k)

If you didn't get the book version it's the substitution method explained here.

  • T(k) = 2T(k - 1) + c
    • T(1) = c0 + c
    • T(2) = 2T(1) + c or 3c
    • T(3) = 2T(2) + c or 7c
    • T(4) = 2T(3) + c or 15c

T(4) is c(2k - 1) or O(2k)

Learning recurrences

If you want to learn recurrences I suggest you take a calculus course. What you're really doing is trying to solve a difference equation which is a discrete analog of ordinary differential equations. Knowing the techniques for solving differential equations thus teaches you how to solve recurrences because they are the same thing. Schools and algorithm texts like to use recurrences because you can take a recursive program and translate it to a math function for analysis using recurrences thus dispensing with all the overhead complexity of programming languages. The problem is they are an awful way to represent programs and the bounds 'proved' don't often reflect the actual bounds and if the students don't have a background in calculus none of it makes sense. For example many differential equations are solved by multiplying by an integrating factor then integrating and many recurrences are solved by a similar way but they never teach you this instead magic happens where they cherry pick a perfect factor to divide/multiply both sides and everything just works out.

There's a book An Intro to the Analysis of Algorithms by Sedgewick & Flajolet which will teach you that if a problem has a recurrence then the problem has a sufficient enough structure where you can use better tools to develop analytical results. There's a MOOC for that book fully open as well where they show you how to avoid using recurrences completely.

However you will need a calculus course first. A good one is Calculus Revisited by Herb Gross on MIT OCW which I go through here (eventually). You can also take the regular MIT 18.01, 18.02 and 18.03 sequence of courses. Yes even though analysis of algorithms is 'discrete math' the tools of calculus are often employed to model loops as differential equations or calculating derivatives to find a sequence associated with any generating function or all the integration tricks used in asymptotics. Calculus of course teaches functions and their growth so big-O will make sense.

Proof by induction is this strange skill where you draw a conclusion based on observed patterns and they never really teach this, instead you just have to consistently be exposed to reading other's proofs using induction to get it. The critical part is the assumption, what can we assume is true. There's a few book that will cover countless proofs of induction like any programming theory book such as Robert Harper's book or this work book that explains the PL foundations in Agda book. Terence Tao's Analysis I will do so as well in the first few chapters you see a hundred simple mathematical induction proofs.

Induction (optional)

Induction was explained by Aristotle in Posterior Analytics and Prior Analytics. In one of the books he says induction (epagoge) is a progression from particulars to a universal. This can mean either forming a concept by abstracting a specific case to a general case or a process of propositional inference where each step infers the next. Aristotle even wrote the definition of induction by using induction having his propositional inference definition depend on the previous account of induction being a general case formation. This is why everyone calls Aristotle a genius its because all his books are all like this so not only is the written text teaching but sometimes the way the books are organized themselves demonstrate the teachings. Aristotle then writes induction can claim to have gone through all particular instances, not because it has actually enumerated every single particular instance, but through the realization that all instances have the same observed property as an essential property so clearly it defacto enumerates all instances. So to go back to his original definition of induction being a progression from particulars to a universal, he means the observations of essential properties that do not change in any instance leading to a general concept (hypothesis) provides the foundation for a statement's universality and not the propositional inferences. Aristotle then in the Topics explains how you would identify such properties. Today the interpretation of induction is propositional inference that leads to a 'creative moment of illumination' where you perform an inductive hypothesis but if you want to see it done without any kind of magic then read Aristotle.

Math induction

Proof by induction is similar to what we've already been doing (recursion on lists). You show a property holds for the base case (empty case), you assume the property holds for any arbitrary n case, then you prove the property holds in the the n + 1 case. Then base, n and n + 1 create an implication chain that the property holds for all n. This is explained here.

We can prove the Pyret append operator on lists.

  • Theorem: For all lists x and y, length(append(x, y)) = length(x) + length(y)

We first prove the above theorem works if list x is empty (base case):

  • Left hand side of theorem:
    • length(append(empty, y)
    • = length(y)
    • = 0 + length(y)
  • Right hand side of theorem:
    • length(empty) + length(y)
    • = 0 + length(y)
    • = length(y)

Now the n case, in a list data type we have link(f, r) which is f = first or n + 1 case and f = rest or n (hypothesis or n-th case). Using the rest of the list:

  • Assume that for the n case: length(append(x.rest, y)) = length(x.rest) + length(y)

Anytime in our proof when we substitute/move things around, if it is in the form of the above we can substitute one for the other since we assumed they are both equal to each other. Now we prove the original theorem (for all lists x and y ..) for the n + 1 case which is the entire list x:

  • Theorem left side: length(append(x, y))
    • = length(append(append(x.first, x.rest), y)) [remove the first]
    • = length(append(x.first, (append(x.rest, y)))) [the same expression as previous]
    • = 1 + length(append(x.rest, y)) [convert append(x.first, rest) to 1]

The last move we used the base case, as we already established earlier length(empty) was 0 + length(y), so here we can use that to say length(first) is 1 + length(append(x.rest, y)). Now we have the form of the earlier assumption, so we can substitute:

  • = 1 + length(x.rest) + length(y)
    • = length(append(x.first, x.rest)) + length(y) [Add back in x.first]
    • = length(x) + length(y) and matches right-hand side of theorem

Proof of bounds (optional)

In the lecture he said the class didn't need to know this yet as it would be taught later in an algorithms analysis course, but we can try some examples.

  • T(k) = T(k - 1) + c
    • O(k)
  • Claim: For all k \(\ge\) 1, T(k - 1) + c \(\le\) ck
  • Base case: k = 1 and T(1 - 1) = c0 + c or 1c which is \(\le\) ck
  • Assume: for arbitrary k, T(k) \(\le\) ck
  • Prove n + 1: T(k + 1) \(\le\) c(k + 1)
    • T(k - 1 + 1) + c \(\le\) c(k + 1)
    • = T(k) + c \(\le\) ck + c
      • Our assumption says T(k) is less than ck so O(k)

Induction variants

Because the logarithmic recurrences are jumping to earlier stages we can't use information about n to prove n + 1 as it's not enough to just prove a property about one previous value of T(k) so we will use a variant called complete or strong induction where you assume all previous inputs up to n hold the property, then this implies n + 1 also holds this property:

Complete induction algorithm:

  • base case not usually needed since we are assuming everything holds up to n but can help with establishing bounds.
  • for all n \(\ge\) base case, if x holds this property: for all (base case) \(\le\) x \(\lt\) n then n also holds this property.

Let's try:

  • T(k) = T(k/2) + c
    • O(log k)

To make this proof easier, let's assign the constant in T(k/2) + c a number and since it's a constant and doesn't matter (we can pick any number) we pick c = 4. Since T(1) is another constant we can pick T(1) = 1. We will derive the real constant later.

  • Claim: For all n \(\ge\) 2, T(n) \(\le\) clog(n)
  • Base case: (n = 2): T(2/2) = T(1) + 4 = 5 = clog(2) = 1c
    • Temporarily pick 5 for c so T(2) \(\le\) 5log(2)
    • 2 was chosen since log(0) is undefined and log(1) is 0 in log2
  • Assume: Let n \(\ge\) 3. For all 2 \(\le\) k \(\lt\) n assume T(k) \(\le\) clog(k)

Our strategy at this point is to somehow build up implications to get the form xn/y where x/y is a rational number less than 3. Our strategy is arrive at the form clog(xn/y) so by log laws we have clog(n) + clog(x/y) + 4. Since this is an algorithm you also assume the inputs are increasing.

  • Recall let n \(\ge\) 3
  • T(n) = T(n/2) + 4 since n \(\ge\) 2 (base case)
  • \(\le\) clog(n/2) + 4
    • 3/2 is less than 2, from our assumption: 3/2 \(\le\) n/2 \(\lt\) n
  • \(\le\) clog(\(\frac{n + 1}{2}\)) + 4 since log is increasing and n/2 \(\le\) (n + 1)/2
  • \(\le\) clog(\(\frac{2n}{3}\)) + 4 since log is increasing and \(\frac{n + 1}{2}\) \(\le\) \(\frac{2n}{3}\) for n \(\ge\) 3
    • Works because (n + 1) \(\le\) 2n implies log(n + 1) \(\le\) log(2n)
    • We get the desired form and can use log laws
  • = clog(n) + clog(2/3) + 4
    • The constant is c \(\ge\) \(\frac{c}{-log_{2}(2/3)}\) + 4

Then 4/(-log(2/3)) is ~7. Change 5log(n) in our base case to 7log(n) and the proof is done:

  • 7log(n) + 7log(2/3) + 4
  • = 7log(n) + -4.09 + 4
  • = 7log(n) + O(1) (or really 0.09, or 0)
    • T(k/2) + c is O(log n)

Although we changed the c in T(k/2) + c to be 4 you can see now it doesn't really matter what it is, you can always multiply clog2(2/3) by some constant to erase the other constant. I can't remember where I originally got this proof from but all I remembered from it was their strategy of building up implications to get to clog(x/y).

That is basically the gist of every single proof of bounds using reccurrences and it was stupid, we had to anticipate a strategy where the constant cancelled out and use inference to rearrange forms just to prove a simple O(log n) bound. My advice is just don't bother, read the DCIC book and recognize those common reccurrences and their bounds. You will never need to use anything else unless writing your own algorithms from scratch and if you do try Sedgewick's book instead and use a better tool kit of analysis avoiding recurrences completely. Watch the MOOC.

Lecture 5 Insertion Sort

We're watching lecture 5 the Mon9/17/18 lecture. The first 30mins or so of the recording doesn't have video, but you can pause the video around 30:14 in, open the video again to hear the audio, and see everything on the board they are working on (sort and insert).

The if-statement you take the worst complexity branch and any dependent programs like sort calling insert, you do analysis on insert first. A link operation link(f, r) takes a constant amount of time regardless of the size of the input list, because it doesn't make a copy of the list.

On the right hand of the board:

  • Tinsert(0) = \(c_0\) (constant steps for empty, or base case)
  • Tinsert(k) = c + Tinsert(k - 1)
  • = c + c + c + … \(c_0\)
  • = kc + \(c_0\)

Sort analysis:

  • Tsort(0) = \(c_0\) or a constant amount of steps
  • Tsort(k) = Tsort(k - 1) + Tinsert(k - 1)c + \(c_0\)
  • = Tsort(k - 1) + kc
  • We've seen this already in the book T(k - 1) + k is the series 1 + 2 + 3 + …+(k - 1) + k or n(n + 1)/2 or \(O(n^2)\)
  • Insertion Sort is \(O(n^2)\) worst case, and O(n) + O(1) in the best case because you still have to call sort(r) and do linear work exploding that n sized list out before you can do the O(1) insert operations.

The quick and dirty way to do this is to realize sort loops k times, and calls insert each time which does linear work so worst-case is O([k -> k*k]) because you always multiply the body of a loop by everything in it. If the body of a loop with k inputs does k work then k * k.

Lab: Big-O 1

As mentioned in lecture there's 2 Big-O labs. Let's try the first see bottom of page here.

The first lab task 1: We can disregard 5 and x in g(x) = 5x2 + x because the definition includes any constant multiplied by g(x), and we are bounding growth, so x2 bounds both x and x2

Task 2: Fill in the table for link(,r) => 1 + len(r) the underscore binding means you aren't using that selector as first of the list is never used in length, we only use r. It doesn't matter how you score the running time of constant operations, the answer is 12k + 4 total for the whole function I get 9k + 4 total and don't know where the other 3 constant operations came from. Either way it's still O(k).

Task 3: Look at the table directly above these exercises, also remember you multiply whatever is in the loop by how many times you loop and being that these are recursive programs the entire program is a loop.

Task 4: Find the closed form.

  • T(k) = T(k - 2) + 1
    • Anything less or equal to 1 is c0 or assume 0
  • T(k - 2) + 1 + T(0) or 1
  • T(k - 3) + 1 + T(1) or 1
  • T(k - 4) + (k - 2) + 1 or 2
  • T(k - 5) + (k - 3) + 1 or 2
  • T(k - 6) + (k - 4) + (k - 2) + 1 or 4
  • 1, 1, 2, 2, 4, 4, 8, 8…
    • Use oeis.org 2floor(k/2)?
  • T(k) = 2T(k - 1) + 1
    • Video solution here

Task 5: Find recurrence/closed form for delete. First you figure out the complexity of remove, which linearly goes through an entire list and removes something if it exists so worst case it's O(n) as it does n comparisons. Then you go through delete and it seems to me that it's T(k - 1) + k where the + k is that remove function being called every single recursion that doesn't shrink with the input size. This is of course k -> k2 from book and 5.1 Solving Recurrences example in this lab. If you go back to the beginning of the lab, they talked about the complexity of member where if a function O(F) makes calls to another function O(G) at every one of its steps, which delete is doing when it calls remove at every one of it's steps, then it's O(F x G) complexity.

The extra recurrences, there are videos for them here or read through some of these notes on the various strategies. You aren't expected to know recurrences for this intro course just enough to recognize certain patterns of program behavior described in the book.

Assignment 4

This assignment data scripting is more practice that mirrors what a typical developer would have to do writing quick basic tasks. If for any reason the assignments go down use the way back machine/archive.org here. This is due before the next lecture in the actual course.

There's a paper for this assignment too, how palindrome needs a data cleaning solution (remember the second lecture/Rainfall problem) and how many other problems could benefit from reshaping data. Try entering 10.1145/2839509.2844556 into sci-hub to read it to see how they would implement most of these problems.

Week 4 Accelerated Intro to CS

We're watching lecture 6 titled Wed9/19/18. The fine details of the program trace in the beginning isn't important he is explaining the structure of linear k -> k vs quadratic k -> k2

He remarks that their quicksort implementation is really a filter, and we should try quicksort using a filter:

He ends the lecture trying to convey a bigger idea: use the structure of the data to come up with a simple solution, confirm the inefficient solution is correct, optimize it according to the bound you want.

fun combine(lt, p, gt):
  lt.append([list: p]).append(gt)
end

fun qsort(l):
  cases (List) l:
    | empty => empty
    | link(pivot, r) =>
      combine(
        qsort(r.filter(lam(n): n < pivot end)),
        pivot,
        qsort(r.filter(lam(n): n >= pivot end)))
  end
end
check:
  qsort([list: 2, 1, 3, 4, 5]) is [list: 1, 2, 3, 4, 5]
  qsort([list: 100, 0, 1, 343]) is [list: 0, 1, 100, 343]
  qsort([list: 0, -1, 100]) is [list: -1, 0, 100]
  qsort([list: ]) is ([list: ])
  qsort([list: 8, 1, 1, 9, -1]) is [list: -1, 1, 1, 8, 9]
end

Lab: Big-O 2

Second lab here. In the lab is another link to slides here about the lower bound on all comparison sort algorithms, how you can never do better than O(n log n). It's surprising this is given in an intro course you usually learn it in an algorithms course much later.

Task: Minimum possible runtimes, you're only supposed to think about why your answer is true. For example to sum all elements in a list you have to look at every element no matter what so it's going to be linear at minimum. The second task is fold2 over both lists again linear or sum the first list, then sum the second and add both their totals: O(n) + O(n) which is still O(n). The third task you linearly go through one list, and at each loop do constant work on the entire second list so n * n. It would look like:

fun product(l1, l2):
  cases (List) l1:
    | empty => 0
    | link(f, r) => 
  l2.foldl(lam(x, acc): acc + (f * x) end, 0) + product(r, l2) end
where:
  product([list: 1, 2], [list: 1]) is 3
end

The task for ab or [list: 1, 2] and [list: 1] to 11 + 21 = 3 try with [list:2, 3] and [list: 3] because 23 + 33 doesn't equal (2*3) + (3*3). Let's say there's a typo or I misinterpreted the problem you'd still have to go through one entire list, and at each loop go through another entire linear list so n*n.

Task: undo-sol k*(complexity of undo-helper) which appears to be linear, each loop you are doing constant work then when finished reverse the list so linear + linear or O(n + n) which is O(n). So undo-sol is k*k.

Can we make sols-silly-flipper linear? Try some examples:

With reversing the list:
1, 2, 3, 4, 5, 6
1, 6, 5, 4, 3, 2
1, 6, 2, 3, 4, 5
1, 6, 2, 5, 4, 3
1, 6, 2, 5, 3, 4

Without reversing the list:
1, 2, 3, 4, 5, 6
1] 6, 2, 3, 4, 5 
1, 6] 2, 3, 4, 5 linked moved elt
1, 6, 2] 3, 4, 5 linked next elt
1, 6, 2, 5] 3, 4 linked moved elt
1, 6, 2, 5, 3, 4] linked next elt

You can use a combo of last, push and take to do this with another function passing the length to the linear flipper which decrements it everytime you recurse or do a take operation dropping elements. Then it's linear + linear which is O(n).

Assignment 5

Let's build a testing oracle. There's a starter template we need to access the function they give us matchmaker() and is-hire():

# CSCI0190 (Fall 2018)
provide *
provide-types *

import shared-gdrive("oracle-support.arr",
  "11JjbUnU58ZJCXSphUEyIODD1YaDLAPLm") as O

##### PUT IMPORTS BELOW HERE ############


##### PUT IMPORTS ABOVE HERE ############

type Hire = O.Hire
hire = O.hire
is-hire = O.is-hire
matchmaker = O.matchmaker

# DO NOT CHANGE ANYTHING ABOVE THIS LINE

fun generate-input(num :: Number) -> List<List<Number>>:
  doc: 
  ```
  generates a list of candidates or companies for a group of size num
  ```
  empty
where:
  generate-input(0) is empty
end

fun is-valid(
    companies :: List<List<Number>>,
    candidates :: List<List<Number>>,
    hires :: Set<Hire>)
  -> Boolean:
  doc: 
  ```
  Tells if the set of hires is a good match for the given
  company and candidate preferences.
  ```
  true
where:
  is-valid(empty, empty, empty-set) is true
end

fun oracle(a-matchmaker :: (List<List<Number>>, List<List<Number>> 
      -> Set<Hire>))
  -> Boolean:
  doc: 
  ```
  Takes a purported matchmaking algorithm as input and outputs whether or
  not it always returns the correct response
  ```
  true
where:
  oracle(matchmaker) is true
end

Run the template then try to give matchmaker some inputs to see what it does:

companies =  [list: [list: 1, 0, 2], [list: 0, 2, 1], [list: 0, 2, 1]]
candidates = [list: [list: 1, 0, 2], [list: 1, 2, 0], [list: 0, 1, 2]]
# Note matchmaker writeup: it is 0 to n-1 indexed, so 3 lists means 0 - 2 

matchmaker(companies, candidates)
>>[list-set: hire(2, 1), hire(1, 0), hire(0, 2)]

The stable matching algorithm is described here from the Algorithm Design book lecture page. We are given this algorithm in the template (note the import shared drive) we don't have to write it, only test it.

  • First round:
  • Companies all select their first preferred candidate
  • Candidates accept provisionally, if a more preferred offer is presented in next rounds they will reject this one
    • Rejected companies are put into a pool
  • Second round:
  • Companies in the pool with no matching then all select their second preferred candidate
  • Candidates accept or reject upon preferences
  • X rounds:
  • Repeat, noting that no company can offer more than once to a candidate

The 2019 assignment writeup contains a few hints, such as 'be cautious about using the provided matchmaker function in your oracle as this assumes that there is only one right answer to a given instance of the stable hiring problem'. This refers to the book using satisfies in tests where you compare the output of one program against another.

I translate this to mean there could be many permutations of the stable-hiring problem which are still correct, however the Gale-Shipley algorithm produces the exact same matching everytime if the company is making offers, and candidates rejecting. What if the roles are reversed, if it is candidate making offers and company rejecting? We have 2 inputs into matchmaker but it doesn't say which one is actually making the offers and which input is rejecting. We should try altering the inputs and see what happens:

companies =  [list: [list: 1, 0, 2], [list: 0, 2, 1], [list: 0, 2, 1]]
candidates = [list: [list: 1, 0, 2], [list: 1, 2, 0], [list: 0, 1, 2]]

>>matchmaker(candidates, companies)
[list-set: hire(2,2), hire(1,0), hire(0,1)]

>>matchmaker(companies, candidates)
[list-set: hire(2,1), hire(1,0), hire(0,2)]

They're different, so there is multiple possible matchings we have to test for. I found an old book entirely on the Stable Marriage problem here and on page 8 (of the book, not the pdf page#): For each company C, look at their preferences and check that each candidate they prefer is not matched to somebody else if candidate also prefers company C. Think about the properties of a stable matching: there should be n companies matched with n candidates, there should not be any double matching or missing matchings.

Solutions

The solutions or what kind of tests they expected to see are all in the earlier paper we read (see 4.2 Matcher). They point out the problem is a relation meaning has many correct outputs whereas a function only has a single output, so the oracle (much like sortacle) tests properties not specific values.

Lecture 7 Trees

We're watchig lecture 7 Fri9/21/18 lecture on 2D tree shaped data w/guest lecturer who teaches the Logic for Systems course at Brown. An interesting paper from his page authored with Prof K you should read is The Human in Formal Methods. In it they write about porting the method of example writing we've been doing to this domain. A common learning mistake that students make is talked about, in that they write programs before they have understood the problem, as a result they 'solve' the wrong problem, which misdirected from the learning goals.

The mutual recursive datatypes, to understand them write out your own tree:

       a 
      /  \   
    b    none     
   /  \
  c    none
   \ 
    none

a has child b and none
b has child c and none
c has none
  • start with a:
  • person("a", No-children)
  • add in b:
  • person("a", child(person("b", No-children), No-children))
  • add in c:
  • person("a", child(person("b", child(person("c", No-children), No-children)), No-children))

Lecture 8 Sets

We're watching lecture 8 Mon9/24/18 lecture on sets. This is a design lecture, given something that has no notion of order, how do you design it's data structure representation. The end of this lecture discussing the big-O complexity of both representations he points out the inefficient representation is better depending on what you care about, like appending a log you often don't look at.

Week 5 Accelerated Intro to CS

We're watching lecture 9 Wed/9/26/18. This is a light lecture just explaining the intuition behind logarithmic complexity to set up balanced binary search trees. At 7:10 he explains logarithms: "A (base-10) logarithm of a number is essentially how many digits a number has". He then explains in terms of virtual machine costs, if you have a logarithmic algorithm and there is a million new runs of that algorithm you are only paying 6x the cost.

Assignment 6

This is a standard assignment creating a filesystem directory tree/model.

I was curious and wanted to write a method meaning a .function() type built-in like l.length(), Pyret allows you to write methods for datatypes here's an example for du-dir() where I wrote a .size() method:

Click to see my example of size

provide *
provide-types *

data File:
  | file(name :: String, size :: Number, content :: String)
end

data Dir:
  | dir(name :: String, ds :: List<Dir>, fs :: List<File>) with:
    method size(self :: Dir) -> Number:
      doc: "Returns combined file size, file count, and sub directory count of a Dir"
      size(self)
    end
end

fun size(d :: Dir) -> Number:
  doc: "Method .size()"

  fun fsize(fd :: Dir) -> Number:
    doc: "Returns file size, and number of files in a Dir"
    fd.fs.foldl(lam(f :: File, acc :: Number): 1 + f.size + acc end, 0)
  end

  fsize(d)
    + map(lam(x :: Dir): 1 + size(x) end, d.ds).foldl(lam(elt, acc): elt + acc end, 0)
end
check:
  a = dir("TS", [list:
      dir("Text", empty, [list: file("part1", 1, "content")])], empty)

  b = dir("Libs", [list: 
      dir("Code", empty, [list: file("hang", 1, "content"), file("draw", 1, "content")]), 
      dir("Docs", empty, [list: file("read!", 1, "content")])], empty)

  c = dir("TS", [list:
      dir("Text", empty, [list: file("part1", 1, "content"), file("part2", 1, "content"), file("part3", 1, "content")]),   
      dir("Libs", [list: 
          dir("Code", empty, [list: file("hang", 1, "content"), file("draw", 2, "content")]), 
          dir("Docs", empty, [list: file("read!", 1, "content")])], empty)],
    [list: file("read!", 1, "content")])

  d = dir("TS", [list:
      dir("Text", empty, [list: file("part1", 99, "content"), file("part2", 52, "content"), file("part3", 17, "content")]),   
      dir("Libs", [list: 
          dir("Code", empty, [list: file("hang", 8, "content"), file("draw", 2, "content")]), 
          dir("Docs", empty, [list: file("read!", 19, "content")])], empty)],
    [list: file("read!", 10, "content")])

  e = dir("TS", empty, [list: file("", 1, "")])

  a.size() is 3
  b.size() is 8
  c.size() is 19
  d.size() is 218
  e.size() is 2
end

I looked at the Pyret source on github to see how to chain multiple methods:

data Dir:
  | dir(name :: String, ds :: List<Dir>, fs :: List<File>) with:
    method size(self :: Dir) -> Number:
      doc: "Returns the size of all sub directories"
      size(self)
    end,
    method path(self :: Dir, s :: String) -> List<Path>:
      doc: "Takes a Dir, and a search string, returns the path"
      path(self, s)
    end
end

Lecture 10 Balanced Binary Search Trees

Watching lecture 10 Fri/9/28/18. We discover the entire last lecture was just trolling and would not give us logarithmic time in terms of successfully being able to throw away half the data because the tree isn't balanced. This whole lecture is understanding how you can balance a tree.

Lab: Tree Traversal

Let's try the CS19 lab for trees. The first part is just thought exercises then the lab offers us stencil code for the BT tree lab sections:

#  _____                _                                      _ 
# |_   _| __ ___  ___  | |_ _ __ __ ___   _____ _ __ ___  __ _| |
#   | || '__/ _ \/ _ \ | __| '__/ _` \ \ / / _ \ '__/ __|/ _` | |
#   | || | |  __/  __/ | |_| | | (_| |\ V /  __/ |  \__ \ (_| | |
#   |_||_|  \___|\___|  \__|_|  \__,_| \_/ \___|_|  |___/\__,_|_|

data BTree<T>:
  | mt
  | node(value :: T, left :: BTree<T>, right :: BTree<T>)
end

fun btree-in-order<A>(tree :: BTree<A>) -> List<A>:
  doc: "Returns the elements of tree in a list via an in-order traversal"
  ...
where:
  nothing
end

fun btree-pre-order<A>(tree :: BTree<A>) -> List<A>:
  doc: "Returns the elements of tree in a list via a pre-order traversal"
  ...
where:
  nothing
end

fun btree-post-order<A>(tree :: BTree<A>) -> List<A>:
  doc: "Returns the elements of tree in a list via a post-order traversal"
  ...
where:
  nothing
end

First make a tree, run it, and type 'a-tree' into pyret interpreter and click the output to see the right and left branches, or a-tree.left or a-tree.right

a-tree =
  node(5,
    node(4, 
      node(3, mt, mt), mt),
    node(6, node(7, mt, mt), node(8, mt, mt)))

>> a-tree.left
node(4, node(3, mt, mt), mt)

>> a-tree.right
node(6, node(7, mt, mt), node(8, mt, mt))

>> a-tree.value
5

The lab wants us to write various traversals, here's what I wrote for btree-in-order which is basically the other two orders as well, just rearrange helper(l) + root + helper(r):

data BTree<T>:
  | mt
  | node(value :: T, left :: BTree<T>, right :: BTree<T>)
end

a-tree =
  node(5,
    node(4, 
      node(3, mt, mt), mt),
    node(6, node(7, mt, mt), node(8, mt, mt)))

# degenerate tree from DCIC book
b-tree = node(1, mt,
  node(2, mt,
    node(3, mt,
      node(4, mt, mt))))

fun btree-in-order<A>(tree :: BTree<A>) -> List<A>:
  doc: "Returns the elements of tree in a list via an in-order traversal"

  fun helper(n :: BTree<A>) -> List<A>:
    cases (BTree) n:
      | mt => empty
      | node(v, l, r) =>
        link(v, helper(l) + helper(r)) 
    end
  end

  cases (BTree) tree:
    | mt => empty
    | node(v, l, r) =>
      helper(l) + link(v, empty) + helper(r)
  end
where:
  btree-in-order(a-tree) is [list: 4, 3, 5, 6, 7, 8]
  btree-in-order(b-tree) is [list: 1, 2, 3, 4]
end

Another lab where we rewrite map/filter/fold. When you see Prof K's lecture on knowledge transfer you'll understand why he makes us do this over and over. This is my first attempt at map, I wrote it like he writes software on the board in class thinking through the data definition:

data BTree<T>:
  | mt
  | node(value :: T, left :: BTree<T>, right :: BTree<T>)
end

a-tree =
  node(5,
    node(4, 
      node(3, mt, mt), mt),
    node(6, node(7, mt, mt), node(8, mt, mt)))

# degenerate tree from DCIC book
b-tree = node(1, mt,
  node(2, mt,
    node(3, mt,
      node(4, mt, mt))))

fun btree-map<A, B>(f :: (A -> B), tree :: BTree<A>) -> BTree<B>:
  doc: "Recursively applies f to the value of every node contained in tree"

  cases (BTree) tree:
    | mt => mt
    | node(v, l, r) =>
      node(f(v), btree-map(f, l), btree-map(f, r))
  end
where:
  btree-map(lam(x): x + 1 end, a-tree) is node(6, node(5, node(4, mt, mt), mt), 
    node(7, node(8, mt, mt), node(9, mt, mt)))
  btree-map(lam(x): x + 1 end, b-tree) is node(2, mt, node(3, mt, node(4, mt, node(5, mt, mt))))
end

>>btree-map(lam(x): x * x end, a-tree)
node(25, node(16, node(9, mt, mt), mt), node(36, node(49, mt, mt), node(64, mt, mt)))

Note that btree-fold the lab wants you to convert the tree to a list, apply f to every element and produce a value just like regular fold does with a warning about complete testing, meaning every function of regular fold should be replicated here.

Assignment 7

Reading the assignment for updater, the Cursor datatype can also contain another cursor which points to the node above:

data Cursor<A>:
  | mt-cursor
  | cursor(above :: Cursor<A>, index :: List<Number>, point :: Tree<A>)
end

The index represents the pointed to cursor's position in the child list, ie: [list: 0]. This means you can fold through all the cursor.above indexes to make a string of .get() or .set()::

import lists as L

data Tree<A>:
  | mt
  | node(value :: A, children :: List<Tree>)
end

data Cursor<A>:
  | mt-cursor
  | cursor(above :: Cursor<A>, index :: List<Number>, point :: Tree<A>)
end

# test tree:
#        1
#       / \
#      2   5
#     / \   \
#    4   3   6 
#         \   \
#         3.5  7

test-tree = 
  node(1, [list:
      node(2, 
        [list: node(4, empty), node(3, [list: node(3.5, empty)])]),
      node(5, 
        [list: node(6, [list: node(7, empty)])])])

# left side
test-tree.children.get(0)

# right side
test-tree.children.get(1)

# get node 3.5
test-tree.children.get(0).children.get(1).children.get(0)

# produces a list of indices from root to cur.point ex: [list: 0,1,0] 
fun get-index(cur :: Cursor):
  L.foldl(lam(_, x): get-index(cur.above) + [list: x] end, empty, cur.index)
end

# produces tree.children.get(n).tree.children.get(n)..
fun get-nodes(tree, l):
  L.fold(lam(t, x): t.children.get(x) end, tree, l)
end

check "map n produces [list: 0, 1]":
  get-nodes(test-tree, map_n(lam(n, _): n end, 0, test-tree.children)) 
    is node(3, [list: node(3.5, empty)])
end

Check the top node, if a match then cursor(mt, empty, entire-tree) otherwise send the children list of the root node into a helper function that keeps track of the parent node. When pred(f.value) is true, find the position in the parent list of your cursor point, extract this number as an index, build the cursor with:

cursor(find-cursor(entire-tree, lam(x): x == parent-node.value end)),
  index-of-point-node, point-node)

The index can also just be a number you test to see if it's 0, meaning trying to move left should raise an error as per assignment. The depth-first search requirements you'll have to figure out a scheme to return the leftmost or topmost node for multiple matches you can get-index all the matching cursors if you want or design find-cursor to only search as per requirements. This program has multiple different ways you can implement I came up with a few before trying to figure out the most efficient way.

get-node-val returns type option (in the DCIC book chapter on sets) an option will wrap the response in some(v) or read the documentation for cases on (Option) with some(a) => syntax. Including an underscore in cases means you don't plan on using that field

# this returns some(v) which you extract with some.value
fun get-node-val<A>(cur :: Cursor<A>) -> Option<A>: 
  cases(Cursor) cur:
    | cursor(_,_, point) => some(point.value) 
    | mt-cursor => none
  end
end

This is one of the best assignments when you get it working and can zip along the tree in constant time using option types as your protection against get'ing a non-existent value. Many languages like OCaml have option types.

Week 6 Accelerated Intro to CS

Watching lecture 11 Mon/10/01/18 about streams. In the examples of ones, everytime you invoke 'ones' you get lzlink(1, <thunk>) which is one, plus a way to get more ones if called again. We're asked to write our own map, you could also try it with cases:

fun maps<A, B>(f :: (A -> B), s :: Lz<A>) -> Lz<B>:
  cases (Lz<A>) s:
    | lzlink(e, r) => lzlink(f(e), {(): maps(f, rst(s))}) 
  end
end

fun maps2<A, B, C>(f :: (A, B -> C), s1 :: Lz<A>, s2 :: Lz<B>) -> Lz<C>:
  cases (Lz<A>) s1:
    | lzlink(e, r) => 
      cases (Lz<B>) s2:
        | lzlink(ee, rr) => lzlink(f(e, ee), {(): maps2(f, rst(s1), rst(s2))})
      end
  end
end

>>s2 = maps(lam(x): x - 1 end, nats)
>>take(s2, 5)
[list: -1, 0, 1, 2, 3]

>>s3 = maps2(lam(x, y): x + y end, nats, nats)
>>take(s3, 5)
[list: 0, 2, 4, 6, 8]

Lab: Streams

The streams lab is here, mostly adapted from the DCIC book. We already wrote map and map2 in the lecture/book, try to write your own filter/fold for streams:

# This is all in the book/lecture on streams
data Stream<T>:
  | lz-link(h :: T, t :: ( -> Stream<T>))
end

rec ones = lz-link(1, lam(): ones end)

fun nats-from(n :: Number):
  lz-link(n, {(): nats-from(n + 1)})
end

nats = nats-from(0)

fun lz-first<T>(s :: Stream<T>) -> T: s.h end
fun lz-rest<T>(s :: Stream<T>) -> Stream<T>: s.t() end

fun take<T>(n :: Number, s :: Stream<T>) -> List<T>:
  if n == 0:
    empty
  else:
    link(lz-first(s), take(n - 1, lz-rest(s)))
  end
end

# Lab assignments
fun lz-fold<A, B>(f :: (A, B -> A), base :: A, s :: Stream<B>) -> Stream<A>:
  lz-link(f(base, lz-first(s)), lam(): lz-fold(f, f(base, lz-first(s)), lz-rest(s)) end) 
end

fun lz-filter<A>(f :: (A -> Boolean), s :: Stream<A>) -> Stream<A>:
  if f(lz-first(s)) == true:
    lz-link(lz-first(s), lam(): lz-filter(f, lz-rest(s)) end)
  else:
    lz-filter(f, lz-rest(s))
  end
end

>> take(10, lz-filter(lam(x): if x > 10: true else: false end end, nats))
[list: 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]

>> take(10, lz-fold(lam(x, acc): acc + x end, 0, nats))
[list: 0, 1, 3, 6, 10, 15, 21, 28, 36, 45]

Assignment 8

The continued fractions assignment.

You only have to use repeating-stream for phi and e, though I wrote a decimal to continued fraction algorithm I got from from here on page 13 and my own inefficient e generator anyway. When you write your repeating-stream function for e it has to be property-based tested since it's a stream you can only take x amount of values at a time.


fun c-frac(n :: Number) -> Stream<Option<Number>>:
  doc: "Consumes decimal, returns some(fraction integral) stream"
  integral = num-truncate(n)
  decimal = n - integral
  ask:
    | decimal > 0 then:
      new-decimal = 1 / decimal
      lz-link(some(integral), {(): c-frac(new-decimal)})
    | otherwise:
      lz-link(some(integral), {(): mt-stream})
  end
where:
  cf-e :: Stream<Number> = c-frac(generate-e(30))
  cf-phi :: Stream<Number> = c-frac((1 + num-sqrt(5)) / 2)

  take(cf-e, 6) is [list: some(2), some(1), some(2), some(1), some(1), some(4)]
  take(cf-phi, 3) is take(repeating-stream([list: some(1)]), 3)
  take(c-frac(2.875), 4) is [list: some(2), some(1), some(7), none]
  take(c-frac(PI), 5) is [list: some(3), some(7), some(15), some(1), some(292)]
end

# see wikipedia it is the sum of 1/k! where k starts at 0 and keeps going
# 1/0! + 1/1 + 1/(1*2) + 1/(1*2*3) ... where 1/0! is 1.
fun generate-e(n :: Number) -> Number:
  doc:"Generates e (inefficiently) to n decimal places"

  fun e-helper(current-count):
    factorial = take(nats-from(1), current-count)
      .foldr(lam(acc, x): x * acc end, 1)
    lz-link(1 / factorial, {(): e-helper(current-count + 1)})
  end

  take(e-helper(1), n).foldl(lam(acc,x): acc + x end, 1)
where:
  epsilon = 0.0000001
  num-abs((generate-e(10) - 2.7182818)) < epsilon is true

  wikipedia-value = 2.71828182845904523536028747135266249775724709369995
  silly-epsilon = 0.00000000000000000000000000000000000000001
  num-abs(generate-e(60) - wikipedia-value) < silly-epsilon is true
end

check "property-based-test-e":  
  # Property-based testing
  # Any frac stream of e, if integral > 1 it should be even:
  # ie [2, 1, 2, 1, 1, 4, 1, 1, 6, 1, 1, 8, 1, 1...]

  fun e-test(e-value, size-of-approx):
    take(c-frac(e-value), size-of-approx)
      .map(lam(x): if x.value > 1: num-modulo(x.value, 2) == 0 else: 0 end end)
      .filter(lam(y): not(y == 0) end)
      .foldl(lam(z, acc): z and acc end, true)
  end

  e-test(generate-e(10), 10) is true
  e-test(generate-e(20), 20) is true
  e-test(generate-e(30), 30) is true
  # This is false because we need a bigger e generated
  e-test(generate-e(20), 30) is false
end

The ask check decimal > 0 was to avoid division by zero, and to terminate finite rational streams with none. PI is a Pyret built-in constant.

The goal of this assignment is to produce a program that can make refined approximations:

Click to see my version of c-approx

# First attempt without Options

fun c-approx(c-list :: List<Number>) -> Number:
  doc: "Consumes list of continued fractions, returns rational approximation"

  cases (List) c-list.rest
      | empty => c-list.first
      | link(f, r) => (1 / c-approx(c-list.rest)) + c-list.first
    end
where: 
  # phi test of approximations
  cf-phi :: Stream<Number> = c-frac((1 + num-sqrt(5)) / 2)

  c-approx(take(cf-phi, 3)) is 3/2
  c-approx(take(cf-phi, 4)) is 5/3
  c-approx(take(cf-phi, 5)) is 8/5
  c-approx(take(cf-phi, 6)) is 13/8

  # PI test of approximations
  c-approx(take(c-frac(PI), 1)) is 3
  c-approx(take(c-frac(PI), 2)) is 22/7
  c-approx(take(c-frac(PI), 3)) is 333/106
  c-approx(take(c-frac(PI), 4)) is 355/113 
end

The algorithm for this is just hand stepping the examples in the assignment writeup:

  • c-approx([list: 3, 7, 15])
    • (1 / c-approx([list: 7, 15]) + 3
    • (1 / (1 / c-approx([list: 15]) + 7) + 3
    • (1 / ((1 / 15) + 7)) + 3
    • (1 / (106/15)) + 3
    • (15/106) + 3
    • 333/106

There is many other stream functions left for you to write like fraction-stream which takes a stream as a parameter, so you can use take() inside the body of fraction-stream to keep grabbing more input:

Click to see my implementation of fraction-stream

# you can use a map over a stream here too

fun fraction-stream(n :: Stream<Number>) -> Stream<Number>:
  doc: "Consumes stream of continued fraction integrals, returns stream of rationals"

  fun helper(integrals, take-ctr):
    next-approx = take(integrals, take-ctr)
    lz-link(c-approx(next-approx), {(): helper(n, take-ctr + 1)})
  end
  helper(n, 1)
where:
  take(fraction-stream(repeating-stream([list: 1])), 3) is [list: 1, 2, 3/2]
end

The assignment has you redoing everything for option types, meaning doing cases on Options to safeguard against division by zero and many other problems the assignment writeup notes. This is another very good assignment showing how you can use a stream anywhere in a program passing it around as a function.

Lecture 12 Model View Controllers

This is lecture 12 Wed10/3/18. You can't see the entire whiteboard in the beginning but it doesn't matter, this is a lecture about seperation of concerns when building large systems, how to embody the essence of a system into a model that is common to all parts of the system.

If you combine this lecture with the streams we just learned, you now know web programming.

Lecture 13 Differential Systems

Watching lecture 13 Fri10/05/18. Using same reactor model to make a point about differential systems. Interesting examples about PID controllers/reactive systems, event loops, checkpoints of the state of the system.

Week 7 Accelerated Intro to CS

Our next assignment is JoinLists but first there's a talk by Guy L. Steele here from 2009 under Supplementary Material you can watch or type that DOI into sci-hub to read the paper, the JoinLists assignment was derived from this. You can skip through the talk and watch whatever is interesting.

  • Big message
    • Effective parallelism uses trees
    • User-defined associative combining operators are good
    • MapReduce is good (we build MapReduce ourselves after this)
    • 'Accumulator' paradigm is obsolete, use divide-and-conquer
    • The last 50 years of programming tricks should be thrown out

Locality is mentioned @11:45, it's explained here at 44:07, a program is likely to use data or instructions near memory locations it has recently used, like referencing array memory locations in succession (stride-1 pattern).

In this Guy L Steele lecture cons is list constructor (we used it week 1 during placement) similar to link(first, rest) and car means first, cdr means rest. His split function takes a lamdba/functional as a parameter like map or filter does. Whenever you hear the term 'catamorphism' (category theory) in this talk, think a fold function on lists or reducing some structure to a single value.

There's some slides using Fortress lang, the function signature syntax is similar to Pyret. Example reductionFilter():

Fortress:
reductionFilter[E](p: E -> Boolean, xs:List[E]):List[E] = 
 ... body
end

Pyret:
fun reductionFilter<A>(f :: (A -> Boolean), xs :: List<A>)-> List<A>:
 ... body
end

These are only used for examples for the greater talk subjects, such as @34:29 notice the unshuffle algorithm is similar to our joinLists assignment datatype containing a number. He chopped up the data and mapped it into a richer dataspace, did all the computations there then projected it back.

Associativity of these operators means if you run (r, 2) concat (5) then it's the same as (r) concat (2,5). A 'monoid' is a term for associative and identity algebras that form the monoid laws, you have some type<A>, an associative operator like concat or + that combines two of them into a single type<A>, and an identity value. In math an identity for natural numbers is n times 1, since multiplying anything by one gives you back that same number. In Pyret every list is linked to an empty list, and if you take [list: 1,2] and link it to empty, you get back [list: 1, 2] another identity.

He quickly goes through a huge amount of slides about string splitting algorithms, but the summary we are interested in is at 51:00 and at 53:49.

  • sums, loops and list constructors are abstractly the same thing:
    • generate a bunch of singletons
    • map over them applying some function
    • combine the results ie: generate and reduce

If you combine the results w/associative operators then you can take advantage of parallelism. The rest of the talk brings up some interesting ideas since function composition is associative: f(g(x)) = g(f(x)) you can create thousands of associative functions that transform your data in steps and with parallelism the order in how this is done doesn't matter thanks to associativity. He talks about how arrays are unsuitable for most of this since they rely on addition hardware, whereas trees are much more efficient. Any kind of accumulators are to be avoided if you are doing multithread/parallel algorithms.

Assignment 9

From the assignment writeup, use the template where join-list(), JoinList.join() and split() are already libraries there for us to use, and some other testing functions. The writeup wants us to use split() for everything if possible. From Guy L Steele's talk we want to generate a bunch of singletons and throw them at functions, and avoid any kind of accumulators if we can.

Make some test JoinLists to play around with:

size-1 = one(1)
size-2 = join-list(one(1), one(2), 2)
size-3 = join-list(join-list(one(3), one(2), 2), one(1), 2)
size-4 = join-list(one("a"), join-list(join-list(one("b"), one("c"), 2), one("d"), 2), 2)

# x gives the first element
# y gives the rest of the JoinList
split(size-3, lam(x, y): y end)
>>> one(1)

size-1.join(one("another"))
>>> join-list(one(1), one("another"), 2)

# identity operator
empty-join-list.join(one("test"))
>>> one("test")

Here's some of what I tried as an example of how to write parallel style:

fun j-first<A>(jl :: JoinList<A>%(is-non-empty-jl)) -> A:
  doc:"Returns the first element of a non-empty list"
  cases(JoinList) jl:
    | one(elt) => elt
    | join-list(_,_,_) => split(jl, lam(x, _): j-first(x) end)
  end
where:
  split(size-2, lam(x, _): j-first(x) end) is 1
  split(size-3, lam(x, _): j-first(x) end) is 3
end

# Uses the identity operator of empty-join-list
fun j-rest<A>(jl :: JoinList<A>%(is-non-empty-jl)) -> JoinList<A>:
  doc:"Returns a join list containing all elements but the first of a non-empty list"
  cases(JoinList) jl:
    | one(elt) => empty-join-list 
    | join-list(_,_,_) =>
      split(jl, lam(x, y): j-rest(x).join(y) end)
  end
where:
  j-rest(size-2) is one(2)
  j-rest(size-3) is join-list(one(2), one(1), 2)
  j-rest(size-4) is join-list(join-list(one("b"), one("c"), 2), one("d"), 2)
end

For j-nth, the logic was return the first element automatically if it's 0, then fall into the join-list case. If the first element's length is smaller or equal than the 0-based indexed n that's given as an argument to j-nth, what we're looking for isn't there so skip the first element (which could be an entire embedded JoinList) and call j-nth again with n adjusted to subtract x length. This way there is no centralized accumulator/counter and you could make 20 of these programs run concurrently to split up the work as Guy L Steele recommended. Try hand stepping this:

fun j-nth<A>(jl :: JoinList<A>%(is-non-empty-jl), n :: Number) -> A:
  doc:"Returns the nth element (0-based index) of a n+1 JoinList"
  cases(JoinList) jl:
    | one(elt) => elt
    | join-list(_,_,_) => split(jl, lam(x, y): x-length = j-length(x)
          if (x-length <= n):
            j-nth(y, n - x-length)
          else: 
            j-nth(x, n)
          end
        end)
  end
end
check:
 size-4 = join-list(one("a"), join-list(join-list(one("b"), one("c"), 2), one("d"), 2), 2)
  j-nth(size-4, 0) is "a"
  j-nth(size-4, 2) is "c"
end

The rest of the assignment is typical exercises we've already done before like implementing map/filter but reduce instead of fold this time. The writeup explains what reduce is, if you have a list [list: 1, 2, 3] then reduce x - y would be 1 - 2 - 3.

The analysis writeup asks 'what property should the first argument to j-reduce demonstrate' and Guy L. Steele already gave us the answer: be associative if you're dealing with manipulating a bunch of singletons and the split function is non-deterministic it can return a different order of join-lists which you will run into if you test j-reduce with '-' or subtraction operator.

Assignment 10

This came out the same day as JoinLists. Before you read the assignment writeup see an example what MapReduce does. We are writing a model of a parallel framework. MapReduce takes two custom functions a mapper and a reducer, and the input. The framework splits the input to feed in parallel to many mapper functions to distribute the work (we don't have to write this). The mapper function returns (key, value) pairs, see this example from the 2016 assignment writeup for a word-count mapper:

check "simple map test":
  wc-map(tv("book1", "the words the words")) is [list: tv("the", 1), tv("words", 1), tv("the", 1), tv("words", 1)]
end

Your collector/filter would then sort these by keys and produce a list of their values: [list: tv("the", [list: 1, 1]), tv("words", [list: 1, 1])] and feed that to reduce which would return tv("the", 2) or tv("words", 2).

Here is my poorly done early draft just to mimic the MapReduce framework and understand it better, we have no idea the result of user supplied reduce so will later have to create property-based tests:

Click to see my implementation

# CSCI0190 (Fall 2018)
provide *

import lists as L
import shared-gdrive("map-reduce-support.arr",
  "1F87DfS_i8fp3XeAyclYgW3sJPIf-WRuH") as support

type Tv-pair = support.Tv-pair
tv = support.tv
wc-map = support.wc-map
wc-reduce = support.wc-reduce

# DO NOT CHANGE ANYTHING ABOVE THIS LINE

## A. Your map-reduce definition

fun map-reduce<A,B,M,N,O>(input :: List<Tv-pair<A,B>>,
    mapper :: (Tv-pair<A,B> -> List<Tv-pair<M,N>>),
    reducer :: (Tv-pair<M,List<N>> -> Tv-pair<M,O>))-> List<Tv-pair<M,O>>:
  doc:""

  fun reduce-prepare<S,V>(distinct :: List<S>, pairs :: List<Tv-pair<S,V>>)-> List<Tv-pair<S,List<V>>>:
    doc: "Map over distinct, filter pairs for matching key, collect all their values"
    distinct.map(lam(key): tv(key, pairs.filter(lam(x): x.tag == key end).map(lam(y): y.value end)) end)
  where:
    reduce-prepare([list: "test", "this"], [list: tv("test", 1), tv("this", 1), tv("test", 1)]) 
      is [list: tv("test", [list: 1, 1]), tv("this", [list: 1])]
  end

  # fold the input feeding it to mapper and appending output into a new list
  key-pairs = input.foldl(lam(elt, acc): acc + mapper(elt) end, empty)
  # get a distinct list of keys
  distinct-keys = L.distinct(key-pairs.map(lam(x): x.tag end))
  # create list of tv(key, List<Value>) to pass to reduce
  reduce-ready = reduce-prepare(distinct-keys, key-pairs)
  # map over tv-pairs and apply reduce returning List<Tv-pair<M,O>>
  reduce-ready.map(lam(x): reducer(x) end)
end

check "map-reduce function test":
  input = [list: tv("filename", "the cars fox"), tv("filename2", "fox cars cars cars")]
  map-reduce(input, wc-map, wc-reduce) is [list: tv("the", 1), tv("fox", 2), tv("cars", 4)] 
end

I was surprised my poor implementation of MapReduce worked fine for my implementation of anagram-map/reduce:

Click to see my version of anagram-map

### B. Your anagram implementation  
  
fun anagram-map(input :: Tv-pair<String, String>) 
  -> List<Tv-pair<String, String>>:
  doc:"Returns Tv-pair list of token sorted key and that token"
  # split on spaces of tv-pair.value 
  tokens = string-split-all(input.value, " ")
  # map over the tokens, create key by sorting token
  map(lam(x): tv(string-from-code-points(string-to-code-points(x).sort()), x) end, tokens)
where:
  # tests taken from a student solution
  anagram-map(tv("words.txt", "star rats tarts")) is 
  [list: tv("arst", "star"), tv("arst", "rats"), tv("arstt", "tarts")]
end

fun anagram-reduce(input :: Tv-pair<String, List<String>>) 
  -> Tv-pair<String, List<String>>:
  doc:"Return key and filter for dupes in key value"
  tv(input.tag, L.distinct(input.value))
end

check "anagram test":
  map-reduce([list: tv("words.txt", "star rats tarts")], anagram-map, anagram-reduce) is 
  [list: tv("arst", [list: "star", "rats"]), tv("arstt", [list: "tarts"])]
  
  # will fail because order isn't exact, need to use lst-same-els built-in 
  map-reduce([list: tv("words.txt", "star rats tarts star")], anagram-map, anagram-reduce) is 
  [list: tv("arst", [list: "star", "rats"]), tv("arstt", [list: "tarts"])]
end

For anagram mapper since the assignment says the same characters is the determination of an anagram we can make it is a key by turning into code points and sorting, then convert back to a string to create a key. The nile mapper/reducer is the same just remember all mapper does is fire out tv(key, value) for every input and reduce either filters for dupes, subtracts or sums values in the form tv(key, [list: values]).

Why would JoinLists be helpful? To split up the input to mapper like in the Wikipedia article where the idea is to split it evenly across hundreds of servers running mapper and feed them all to multiple reducers.

Lecture 14 Identical

Watching lecture Wed10/10/18. A great lecture about memory aliasing, introduces directed acyclic graphs, acyclic meaning it doesn't cycle and has one direction. The reading uses memory location examples to make this concept of more clear

Lecture 15 DAG size, Monads

Watching lecture Fri10/12/18. It starts by going through some task they were given to write examples for determining the size of a DAG that we don't have access to, however this is all in the DCIC book. If a branch is the same construction (recall identical from previous lectures/the book) then you don't count it's size twice.

check "size of A node":
 A = node(1, leaf, leaf)
 size(node(2, A, A)) is 2
 size(node(1, A, A)) is 2

There's only 2 distinct nodes in each, node A and another wrapped around it. Check blocks can have strings, so when you run a test you know which one failed. The rest of this lecture is writing the DAG size function with the class because you have to carry information on which nodes you've already seen. We discover this design pattern of carrying information is called monadic programming.

The end of the lecture he walks through the code of a pyret library he wrote explaining the types and syntax, you can use this to look up the source yourself and read the code.

Week 8 Accelerated Intro to CS

Watching lecture 16 Mon10/15/18 a casual lecture introducing graphs and describing their properties. In fact this whole week is pretty casual just watching lectures he mentions graphs lab is later, and queues lab is after lecture 20.

Lecture 17 Graphs II

Watching lecture 17 Wed10/17/18

His notation on the board for the student samples of implementing data Node is cut off from the recording but you can see yourself why this won't work:

# Node without key
data Node:
  | node(content :: String, lon :: List<Node>)
end

airport = node("EWR", [list: node("PBD", [list: airport])])
# doesn't work, undefined variable   

This is covered in the chapter for DAGs in the DCIC book. We want a level of indirection and that's why graph representations have node keys. This is the function signature for reach:

fun reach(g :: List<Node>, n1 :: Node, found :: (Node-> Boolean))-> Boolean:
    ...
end

'found' parameter is a function you supply, the same as any higher-order function like filter which consumes a function, for some reason this causes confusion in the class and he explains found around the 30m mark. This is all in the DCIC book too.

Lecture 18 Graphs III

Watching lecture Fri10/19/18. The video grows to full size @4:02 you don't miss anything, the first 17:14 mins of the lecture is mainly the class working on the neighbors function contract. @28:53 the reach-3 function is in the book and he explains ormap function, which is also defined in the book. The rest of this lecture he completely walks through reach-3 answering all class questions.

Lecture 19 Queues, Weighted Graphs

Watching lecture Mon10/22/18 we're still on reach-3.

If you can't see the node numbers for the graph on the board:

   1
  /|\
 / | \
2  3  4
 \ | /
   5
 / | \
6  7  8

       (9) <-- disconnected node

He tells us every node is in 3 states: not yet visited (white), completely visited/cycled (black) and visited once but needs to be processed again (grey). He links map/fold/filter to traversal strategies, another reason why we did so many of those in labs. A recursive process he explains automatically gives you depth-first instead of breadth-first, and breadth-first gives you the shortest path to a node for free.

Week 9 Accelerated Intro to CS

Watching lecture 20 Wed10/24/18. Another casual lecture setting up the idea of a spanning tree, an auxillary datastructure you would run to traverse a graph avoiding anything with a cycle. He's giving us many homework hints, reminding us of Oracle assignment when there was multiple correct solutions. 'Test characteristics of the solution not the exact solution' in other words property-based testing.

Prof K gives a good crash course here on peering/internet backbones how it's similar to the idea of lightest next available choice. Now you also know what a greedy algorithm strategy is.

Lab: Queues/Heaps

We don't have access to any of the templates in the queues lab anymore so we will make our own lab.

First read the book chapter Halloween Analysis where a queue implementation is shown.

Amortized analysis is a cost analysis scheme in which the cost of operations over a data structure is specified under the assumption of continued sequential use. A queue has 3 abstract stages: inserted, in transit, and removed for 3n total abstract operations over an n sized sequential run of queuing and dequeuing. The total amortized charge is 3n/3 or 3 on average. Suppose we assign charge 1 to enqueue and charge 2 to dequeue. Most dequeues will have credit or overcharge because we only need to reverse the queues list when we run out of dequeues, and it's all these overcharges that add up to pay for the cost of reversing the list when this data structure is used over and over not a single time.

This is explained formally with induction in an easy to understand manner in Robert Harpers book Programming in SML in chapter 28.2 (and also how the scheme falls apart in the multi-threaded/parallel case, unless you use a cache/memoization which we will soon learn). The general idea is to show the charge function is equal to the real cost of T(n) so establishing an upper bound on the charge function means you can establish an upper bound on T(n).

There is another amortized scheme called the potential method. Potential is a function of the entire data structure, and potential would increase everytime we dequeued and didn't require a list reverse, representing the potential energy stored to do a future heavy cost operation. Once the dequeue list was empty and needed to be replaced by the reversed queue list, the potential would reset to empty 'paying' out for the expensive operation. All of this is explained in this lecture from MIT's 6.046j using a binary counter and 2-3 trees as an example of thinking about the state of the data structure and deciding the worst state when the most likely costliest operation will happen like 2-3 trees where you have all 2 nodes and an insert causes a chain of reaction to split the tree.

To see another potential function/amortized analysis in action let's watch some of this lecture from Erik Demaine's Advanced Data Structures something you can easily complete if you finish CS19 from Brown.

Watching time travel. Up to 11:00 he is describing Pyret, you always have the old data structure (persistence) and any functions on it return a new version that don't alter the original data. @21:20 a back pointer is similar to what we did in the updater assignment, we always had a 'pointer' to the parent. The 'mods' can just be a list where we push previous nodes and keep a time counter somewhere of doing this so we can get() them in constant time with Option types, assuming Pyret returns .get() nodes in O(1). If it doesn't we'll just keep his partial persistence description of only storing up to 2p back pointers in a single node, then creating a new node if there's another change and recursively updating all previous backpointers.

@32:08 this is what we came here for, to see how amortized/potential method works. He explains this quite well, the potential function is defined as the amount of modifications in latest nodes, multiplied by some constant work. The potential charge maxes out to 2cp once you have a full node and decreases by -2p * (constant work) or -2cp work when you generate a new node and recursively change all previous pointers. Thus the cost of those changes is 'paid for' or zero and you are only left with c + c or constant work… over a series of operations run sequentially.

Priority Queues

We have access thanks to Github to previous student solutions, let's look at a Priority Queue they wrote this is from the upcoming MST assignment, cut+paste it into Pyret:

### Heaps Implementation Based on Heap Lab :)
data Edge:
  | edge(a :: String, b :: String, weight :: Number)
end

type Graph = List<Edge>

data Heap:
  | mt
  | node(value :: Edge, left :: Heap, right :: Heap)
end

test-heap = node(edge("a", "b", 1), node(edge("c", "d", 3), 
    node(edge("e", "f", 4), mt, mt), node(edge("g", "h", 5), mt, mt)),
  node(edge("c", "d", 6), node(edge("e", "f", 7), mt, mt), 
    node(edge("g", "h", 8), mt, mt)))

fun insert(el :: Edge, h :: Heap) -> Heap:
  doc: ```Takes in a PossiblePath elt and a proper Heap h produces
       a proper Heap that has all of the elements of h and elt.```
  cases (Heap) h:
    | mt => node(el, mt, mt)
    | node(v,l,r) =>
      if el.weight > v.weight:
        node(v, insert(el, r), l)
      else:
        node(el, insert(v, r), l)
      end
  end
where:
  insert(edge("a", "b", 1), mt) is node(edge("a", "b", 1), mt, mt)
  insert(edge("z", "x", 20), test-heap) is node(edge("a", "b", 1), 
    node(edge("c", "d", 6), node(edge("g", "h", 8), node(edge("z", "x", 20), 
          mt, mt), mt), node(edge("e", "f", 7), mt, mt)), 
    node(edge("c", "d", 3), node(edge("e", "f", 4), mt, mt), node(edge("g", "h",
          5), mt, mt)))
  insert(edge("A", "B", 0), node(edge("a", "b", 1), mt, mt)) is node(edge("A", 
      "B", 0), node(edge("a", "b", 1), mt, mt), mt)
end

fun remove-min(h :: Heap%(is-node)) -> Heap:
  doc: ```Given a proper, non-empty Heap h, removes its minimum element.```
  amputated = amputate-bottom-left(h)
  cases (Heap) amputated.heap:
    | mt => mt
    | node(v,l,r) =>
      reorder(rebalance(node(amputated.elt, l, r)))
  end
where:
  remove-min(test-heap) is node(edge("c", "d", 3), node(edge("c", "d", 6), 
      node(edge("e", "f", 7), mt, mt), node(edge("g", "h", 8), mt, mt)), 
    node(edge("e", "f", 4), node(edge("g", "h", 5), mt, mt), mt))
end

fun rebalance(h :: Heap) -> Heap:
  doc: ```Given a Heap h, switches all children along the leftmost path```
  cases (Heap) h:
    | mt => mt
    | node(v,l,r) => node(v,r,rebalance(l))
  end
where:
  rebalance(node(edge("c", "d", 3), node(edge("c", "d", 6), 
        node(edge("e", "f", 7), mt, mt), mt), mt))
    is node(edge("c", "d", 3), mt, node(edge("c", "d", 6), mt, 
      node(edge("e", "f", 7), mt, mt)))
  rebalance(node(edge("c", "d", 3), mt, node(edge("c", "d", 6), mt, 
        node(edge("e", "f", 7), mt, mt))))
    is node(edge("c", "d", 3), node(edge("c", "d", 6), mt, 
      node(edge("e", "f", 7), mt, mt)), mt)
end

fun get-min(h :: Heap%(is-node)) -> Edge:
  doc: ```Takes in a proper, non-empty Heap h and produces the
       minimum weight in h.```
  cases (Heap) h:
    | mt => raise("Invalid input: empty heap")
    | node(val,_,_) => val
  end
where:
  get-min(test-heap) is edge("a", "b", 1)
end

data Amputated:
  | elt-and-heap(elt :: Edge, heap :: Heap)
end

fun amputate-bottom-left(h :: Heap%(is-node)) -> Amputated:
  doc: ```Given a Heap h, produes an Amputated that contains the
       bottom-left element of h, and h with the bottom-left element removed.```
  cases (Heap) h:
    | mt => raise("Invalid input: empty heap")
    | node(value, left, right) =>
      cases (Heap) left:
        | mt => elt-and-heap(value, mt)
        | node(_, _, _) =>
          rec-amputated = amputate-bottom-left(left)
          elt-and-heap(rec-amputated.elt,
            node(value, rec-amputated.heap, right))
      end
  end
where:
  amputate-bottom-left(test-heap) is elt-and-heap(edge("e", "f", 4), 
    node(edge("a", "b", 1), node(edge("c", "d", 3), mt, node(edge("g", "h", 5), 
          mt, mt)), node(edge("c", "d", 6), node(edge("e", "f", 7), mt, mt), 
        node(edge("g", "h", 8), mt, mt))))
end

fun reorder(h :: Heap) -> Heap:
  doc: ```Given a Heap h, where only the top node is misplaced,
       produces a Heap with the same elements but in proper order.```
  cases(Heap) h:
    | mt => mt
    | node(val, lh, rh) =>
      cases(Heap) lh:
        | mt => node(val, mt, mt)
        | node(lval, llh, lrh) =>
          cases(Heap) rh:
            | mt =>
              if val.weight < lval.weight:
                node(val, node(lval, mt, mt), mt)
              else:
                node(lval, node(val, mt, mt), mt)
              end
            | node(rval, rlh, rrh) =>
              if lval.weight <= rval.weight:
                if val.weight <= lval.weight:
                  h
                else:
                  node(lval, reorder(node(val, llh, lrh)), rh)
                end
              else:
                if val.weight <= rval.weight:
                  h
                else:
                  node(rval, lh, reorder(node(val, rlh, rrh)))
                end
              end
          end
      end
  end
where:
  reorder(mt) is mt
  reorder(test-heap) is test-heap
  reorder(node(edge("a", "b", 20), node(edge("c", "d", 3), 
        node(edge("e", "f", 4), mt, mt), node(edge("g", "h", 5), mt, mt)),
      node(edge("c", "d", 6), node(edge("e", "f", 7), mt, mt), 
        node(edge("g", "h", 8), mt, mt)))) is node(edge("c", "d", 3), 
    node(edge("e", "f", 4), node(edge("a", "b", 20), mt, mt), 
      node(edge("g", "h", 5), mt, mt)), node(edge("c", "d", 6), 
      node(edge("e", "f", 7), mt, mt), node(edge("g", "h", 8), mt, mt)))
end

The term 'heap' here has nothing to do with the same word for a pool of memory you can allocate on a system to implement dynamically sized arrays/objects it just means a tree with an element at each node. A 'min-heap' as described here means the lowest node value it at the root.

Reading 4.2.2 insert, the task asks what's the single thing we can do to reestablish balance if after inserting on the right we end up with n + 1 on the right. My intuition is to just swap the sides of the tree, the right hand side is now the left hand side and balance condition restored. The next task asks why did we insert in the right subheap instead of left, which I assume is because if the condition is left = n and right = n-1 if we insert in the left we get n+1 left and n-1 right an imbalance we can't fix without linearly going through everything and reordering. Note the lab tells us we don't know which case we are in and are inserting blind everytime but we do know the balance condition so can guarantee every right insert and branch swap will keep the condition.

Let's look at the student code:

fun insert(el :: Edge, h :: Heap) -> Heap:
  cases (Heap) h:
    | mt => node(el, mt, mt)
    | node(v,l,r) =>
      if el.weight > v.weight:
        node(v, insert(el, r), l)
      else:
        node(el, insert(v, r), l)
      end
  end

Indeed they have swapped branches node(v, l, r) inserting on the right, then changing with the left to node(v, r, l). Our next task is understand remove-min. Remove that leftmost bottom node to replace the root, swap all left nodes if they exist, then swap left and right branches. Reorder the top node recursively.

Let's look at amputate heap:

data Amputated:
  | elt-and-heap(elt :: Edge, heap :: Heap)
end

fun amputate-bottom-left(h :: Heap%(is-node)) -> Amputated:
  cases (Heap) h:
    | mt => raise("Invalid input: empty heap")
    | node(value, left, right) =>
      cases (Heap) left:
        | mt => elt-and-heap(value, mt)
        | node(_, _, _) =>
          rec-amputated = amputate-bottom-left(left)
          elt-and-heap(rec-amputated.elt,
            node(value, rec-amputated.heap, right))
      end
  end

Do cases on the heap, do cases on the left heap, and keep inserting the left branch into the function until the left is empty and if so use custom datatype 'elt-and-heap' to return that node's value as you must be in the last left node. This student also returned the node beside the left node on the right, in the lab writeup 14 and 21 are beside each other.

Let's look at remove-min, reorder and rebalance operations:

fun remove-min(h :: Heap%(is-node)) -> Heap:
  amputated = amputate-bottom-left(h)
  cases (Heap) amputated.heap:
    | mt => mt
    | node(v,l,r) =>
      reorder(rebalance(node(amputated.elt, l, r)))
  end

Call rebalance with the amputated node then reorder whatever is returned from rebalance.

fun rebalance(h :: Heap) -> Heap:
  cases (Heap) h:
    | mt => mt
    | node(v,l,r) => node(v,r,rebalance(l))
  end

Rebalance moves everything on the right to the left, then recursively swaps the left. Now at this point we are left (as per lab writeup) with 14 on top. Reorder is then called:

fun reorder(h :: Heap) -> Heap:
  cases(Heap) h:
    | mt => mt
    | node(val, lh, rh) =>
      cases(Heap) lh:
        | mt => node(val, mt, mt)
        | node(lval, llh, lrh) =>
          cases(Heap) rh:
            | mt =>
              if val.weight < lval.weight:
                node(val, node(lval, mt, mt), mt)
              else:
                node(lval, node(val, mt, mt), mt)
              end
            | node(rval, rlh, rrh) =>
              if lval.weight <= rval.weight:
                if val.weight <= lval.weight:
                  h
                else:
                  node(lval, reorder(node(val, llh, lrh)), rh)
                end
              else:
                if val.weight <= rval.weight:
                  h
                else:
                  node(rval, lh, reorder(node(val, rlh, rrh)))
                end
              ...

Do cases on the left and right branch from the root. If the right hand branch is empty, look at the left side and return whatever is bigger, root or left branch value. If there is a right hand branch, and if the left value is <= to the right, and if the root <= to the left branch, return the entire heap it's already in order. Otherwise if the root is not smaller than the left hand branch, return the left node as root and rebalance the whole left side. However if the right hand side is bigger then reorder the right side.

Lecture 21 MSTs II

Watching lecture Fri10/26/18 this class is entirely about MST/Dijkstra algorithm examples and property-based test cases you would want to use, and test cases that would be a waste of time. If you forgot any of these algorithms they're in the book in the Graphs chapter. The lesson here is Dijkstra's shortest path algorithm only goes off local information so it's greedy search will never work on a graph with negative weights (which violates the spec of the algorithm anyway) as it doesn't know about them (however we will see in the advanced algorithm course they figured this out in 2022).

Assignment 12

This assignment comes out after lecture 19, and isn't due until lecture 25. He explains this algorithm in lecture 19 around 24mins using a queue with breadth-first traversal and talks about manhattan distance in lecture 20 how it's movement on paths you can travel not a direct line like euclidean distance. The implementation template still works and we have access here. Run that template and try the following commands in the repl:

  • brown-university-landmarks
  • brown-university-landmarks.get("Blueno")
  • brown-university-landmarks.get("Blueno").distance(brown-university-landmarks.get("The-Rock"))
  • brown-university-tours

First task is Dijkstra's algorithm from the book and lectures. Given a name in the graph, construct a set of paths to every place in the graph that is the shortest total distance fom the starting place. You could try a standard linear crawl of the entire graph and get the distances of every place from the starting place and store them in a string-dict as per the assignment writup. Now these are the weights we can use to do a normal greedy strategy shortest-path search where we map/iterate over the string-dict keys and construct shortest path set to every location. So far we have linear crawl + linear map * usual Dijkstra auxillary min-heap and this is O(n + nlog n) or just O(nlog n).

Second task we feed the tours into the same Dijkstra's algorithm but this time we start at a point so must use the distance points function. Feed all the tour places into the same algorithm then begin at the place closest to the starting point. Then we repeat what we did in the first algorithm getting a list of shortest distances and storing them in a StringDict or calculate as you go visiting one point then get'ing the points of every closest node on the tour(s) and continually taking the shortest distance. If you remember the paper we read earlier about this course, in 5.6 Other Examples of PBT 'students implement and test Dijkstra's algorithm; a graph may admit multiple equally-short paths between two vertices' and in the assignment writeup they noted there can be 2 or more places at the same point.

Student solution I found online:


import string-dict as SD
import lists as L
type StringDict = SD.StringDict
string-dict = SD.string-dict


### Campus Tour Helper
fun campus-tour-helper(current :: Name, to-visit :: Set<Name>,
    dijk-res :: Traverse, graph :: Graph) -> Path:
  doc:```consumes the current node, the nodes to visit, the dijkstra results,
      and the graph; returns the path as defined in the problem statement```
  destinations = to-visit.to-list()
  cases (List) destinations:
    | empty => empty
    | link(_, _) =>
      nearest-dest = L.foldl(lam(acc, x): 
          if dijk-res.dist.get-value(x) < dijk-res.dist.get-value(acc):
            x
          else:
            acc
          end
        end, destinations.first, destinations)
      dist-to-nodes = dijkstra-helper(nearest-dest, graph, 
        [string-dict: nearest-dest, 0], 
        [string-dict: nearest-dest, [list: nearest-dest]], 
        insert(pp(nearest-dest, 0, [list: nearest-dest]), mt))

      extracted-path = dijk-res.paths.get-value(nearest-dest).reverse().rest
      extracted-path.append(campus-tour-helper(nearest-dest, 
          to-visit.remove(nearest-dest), dist-to-nodes, graph))
  end
  #|
where:
  campus-tour-helper("3", [set: "1", "4"], dijkstra-helper("3", g, 
      [string-dict: "3", 0], [string-dict: "3", [list: "3"]], 
      insert(pp("3", 0, [list: "3"]), mt)), g) is [list: "1", "2", "4"]

  campus-tour-helper("3", empty-set, dijkstra-helper("3", g, 
      [string-dict: "3", 0], [string-dict: "3", [list: "3"]], 
      insert(pp("3", 0, [list: "3"]), mt)), g) is empty |#
end

### Main functions

fun dijkstra(start :: Name, graph :: Graph) -> Set<Path>:
  doc:```consmues a start note and a graph; returns the shortest
      path to each point in the graph from the start```
  nodes = graph.names()
  if nodes.member(start):
    output = dijkstra-helper(start, graph, 
      [string-dict: start, 0], 
      [string-dict: start, [list: start]], 
      insert(pp(start, 0, [list: start]), mt))

    keys = output.paths.keys().to-list()
    list-to-set(map(output.paths.get-value(_), keys))
  else:
    empty-set
  end
end

fun campus-tour(tours :: Set<Tour>, start-position :: Point, 
    campus-data :: Graph) -> Path:
  doc:```consumes a set of tours, a start position, and campus data; returns
      the calculated campus tour as a path```
  all-stops = L.foldl(lam(acc, x): acc.union(x) end, empty-set, 
    map(_.stops, tours.to-list()))

  if all-stops == empty-set:
    empty
  else:
    graph-nodes = map(campus-data.get(_), campus-data.names().to-list())
    cases (List) graph-nodes:
      | empty => empty
      | link(_, _) =>
        start-node = L.foldl(lam(acc, x): if acc.{1} < x.{1}: acc else: 
          x end end, {graph-nodes.first.name; 
            start-position.distance(graph-nodes.first.position)}, 
          map({(x): {x.name; start-position.distance(x.position)}}, 
            graph-nodes)).{0}

        dist-to-nodes = dijkstra-helper(start-node, campus-data, 
          [string-dict: start-node, 0], 
          [string-dict: start-node, [list: start-node]], 
          insert(pp(start-node, 0, [list: start-node]), mt))
        
        if all-stops.member(start-node):
          updated-set = all-stops.remove(start-node)
          link(start-node, campus-tour-helper(start-node, updated-set, 
            dist-to-nodes, campus-data)).reverse()
        else:
          updated-set = all-stops
          link(start-node, campus-tour-helper(start-node, updated-set, 
            dist-to-nodes, campus-data)).reverse()
        end
    end
  end
end

And the helper/testing functions:


# testing graphs

q1 = place("1", point(0,0), [set: "2", "3"])
q2 = place("2", point(1,0), [set: "1", "3", "4"])
q3 = place("3", point(0,1), [set: "1", "2", "4"])
q4 = place("4", point(2,0), [set: "2", "3"])
g = to-graph([set: q1, q2, q3, q4])

r1 = place("1", point(0,0), [set: "2", "3", "5"])
r2 = place("2", point(1,1), [set: "1", "3", "4"])
r3 = place("3", point(1, -1), [set: "1", "2", "4"])
r4 = place("4", point(2, 0), [set: "2", "3", "5"])
r5 = place("5", point(3, 0), [set: "4", "1"])
g1 = to-graph([set: r1, r2, r3, r4, r5])

### dijkstra helper functions and testing functions

# return type for dijkstra-helper so that it may return both distances and paths
data Traverse:
  | trav(dist :: StringDict<Number>, paths :: StringDict<List<Name>>)
end

fun dijkstra-helper(current :: Name, graph :: Graph, 
    distances :: StringDict<Number>,
    paths :: StringDict<List<Name>>,
    queue :: Heap) -> Traverse:
  doc:```takes in the current node, graph, the distances of nodes added to
      solution, paths, and the queue of paths to check; returns the result of
      dijkstra with both distances and paths```
  cases (Heap) queue:
    | mt => trav(distances, paths)
    | node(_, _, _) =>
      curr-place = graph.get(current)
      neighbors = curr-place.neighbors.to-list()

      # add all neighbors not already in the solution to the queue
      updated-queue = L.foldl(lam(acc :: Heap, x :: Name):
          cases (Option) distances.get(x):
            | none => insert(pp(x, distances.get-value(current) + 
                  curr-place.distance(graph.get(x)), 
                  link(x, paths.get-value(current))), acc)
            | some(_) => acc
          end
        end, queue, neighbors)

      minimum = get-min(updated-queue)
      # if not in the solution add it
      cases (Option) distances.get(minimum.loc):
        | none => 
          dijkstra-helper(minimum.loc, graph, distances.set(minimum.loc, 
              minimum.distance), paths.set(minimum.loc, minimum.path), 
            remove-min(updated-queue))
        | some(_) =>
          dijkstra-helper(minimum.loc, graph, distances, paths, 
            remove-min(updated-queue))
      end
  end
where:
  dijkstra-helper("1", g, [string-dict: "1", 0], [string-dict: "1", 
      [list: "1"]], insert(pp("1", 0, [list: "1"]), mt)) is
  trav([string-dict: "1", 0, "3", 1, "2", 1, "4", 2], [string-dict: "1", 
      [list: "1"], "3", [list: "3", "1"], "2", [list: "2", "1"], "4", 
      [list: "4", "2", "1"]])

  dijkstra-helper("5", g, [string-dict: ], [string-dict: ], mt) 
    is trav([string-dict: ], [string-dict: ])
end

fun is-valid-dijk(proposed :: Set<Path>, start :: Name, graph :: Graph) 
  -> Boolean:
  doc:```consumes a proposed dijkstra solution, a start node, and a graph;
      returns whether each path is a valid solution```
  proposed-list = proposed.to-list()
  if (proposed-list.length() == 0) and 
    not(graph.names().to-list().member(start)):
    true
  else:
    if proposed-list.length() == graph.names().to-list().length():
      if not(proposed-list.length() == 0):
        #checks if all are valid paths
        all-valid = L.all(lam(x): valid-path(x, start, graph) end, 
          proposed-list)

        if all-valid:
          our-lengths = L.foldl(lam(acc, x):
              paths = all-paths(start, x, empty, graph)
              lengths = map(length-path(_, graph), paths)
              min = L.foldl(num-min, lengths.first, lengths)
              acc.set(x, min)
            end, [string-dict: ], graph.names().to-list())

          #checks all lengths
          L.all(lam(x): our-lengths.get-value(x.first) == length-path(x, 
              graph) end, proposed-list)
        else:
          false
        end
      else:
        true
      end
    else:
      false
    end
  end
where:
  is-valid-dijk([set: [list: "4", "2", "1"], [list: "3", "1"], [list: "2", "1"],
      [list: "1"]], "1", g) is true
  is-valid-dijk([set: [list: "4", "2", "1"], [list: "3", "1"], 
      [list: "2", "1"]], "1", g) is false
  is-valid-dijk([set: [list: "4", "2"], [list: "3", "1"], [list: "2", "1"],
      [list: "1"]], "1", g) is false
  is-valid-dijk([set: [list: "4", "2", "3", "1"], [list: "3", "1"], 
      [list: "2", "1"], [list: "1"]], "1", g) is false
end

fun valid-path(path :: Path, start :: Name, graph :: Graph) -> Boolean:
  doc:```given a path, a start, and a graph determines if it is a valid path```
  if path.length() == 0:
    false
  else:
    if path.length() == 1:
      path.first == start
    else:
      is-next-adj = graph.get(path.first).neighbors.member(path.get(1))
      is-next-adj and valid-path(path.rest, start, graph)
    end
  end
where:
  valid-path(empty, "1", g) is false
  valid-path([list: "1"], "1", g) is true
  valid-path([list: "2"], "1", g) is false
  valid-path([list: "4", "2", "3", "1"], "1", g) is true
  valid-path([list: "4", "1"], "1", g) is false
end

fun length-path(path :: Path, graph :: Graph) -> Number:
  doc:```consumes a path and a graph; determines the length of the path```
  cases (List) path.rest:
    | empty => 0
    | link(f, r) =>
      graph.get(path.first).distance(graph.get(f)) 
        + length-path(path.rest, graph)
  end
where:
  length-path([list: "1"], g) is 0
  length-path([list: "4", "2", "1"], g) is 2
end

fun all-paths(start :: Name, target :: Name, seen :: Path, graph :: Graph) 
  -> List<Path>:
  doc:```consumes a start node, target, node, the seen, and the graph;
      returns a list of all Paths to the target from the current```
  if not(start == target):
    current-place = graph.get(start)
    neighbor-paths = map(lam(x): if seen.member(x): empty else: 
          map(link(start, _), all-paths(x, target, link(start, seen), graph)) 
      end end, current-place.neighbors.to-list())
    L.foldl(lam(acc, x): acc.append(x) end, empty, neighbor-paths)        
  else:
    [list: [list: target]]
  end
where:
  all-paths("1", "1", empty, g) is [list: [list: "1"]]
  all-paths("1", "4", empty, g) is [list: [list: "1", "2", "3", "4"], 
    [list: "1", "2", "4"], [list: "1", "3", "2", "4"], [list: "1", "3", "4"]]
end

### campus-tour helper and testing functions
fun is-valid-tour(proposed :: Path, tours :: Set<Tour>, 
    start-position :: Point, graph :: Graph) -> Boolean:
  doc:```consumes a proposed tour, and the input for campus-tour; returns 
      whether the result is a valid tour```
  all-stops = L.foldl(lam(acc, x): acc.union(x) end, empty-set, 
    map(_.stops, tours.to-list()))
  stops-left = L.distinct(L.filter({(x): all-stops.to-list().member(x)}, 
      proposed).reverse())

  if not(all-stops == empty-set) and not(proposed == empty):
    if valid-path(proposed, proposed.last(), graph):
      if all-stops.to-list().length() == stops-left.length():
        graph-nodes = map(graph.get(_), graph.names().to-list())
        distances = map({(x): {x.name; start-position.distance(x.position)}}, 
          graph-nodes)
        minimum-dist = L.foldl(lam(acc, x): if acc.{1} < x.{1}: acc else: 
          x end end, {graph-nodes.first.name; 
            start-position.distance(graph-nodes.first.position)}, distances).{1}

        all-minimal = map({(x): x.{0}}, L.filter({(y): y.{1} == minimum-dist}, 
            distances))
        start-valid = all-minimal.member(proposed.last())
        if start-valid:
          is-valid-tour-on-path(proposed.last(), L.filter(_ == proposed.last(), 
              stops-left), proposed.reverse(), graph)
        else:
          false
        end
      else:
        false
      end
    else:
      false
    end
  else:
    false
  end
where:
  tours = [set: tour("one", [set: "1"]), tour("two", [set: "4"])]
  is-valid-tour([list: "1", "2", "4", "2"], tours, point(0.6, 0.6), g)
    is true
  is-valid-tour([list: "4", "2", "1", "3"], tours, point(0.6, 0.6), g)
    is true
  is-valid-tour([list: "1", "2", "1", "3"], tours, point(0.6, 0.6), g)
    is false
end

fun is-valid-tour-on-path(current :: Name, stops-left :: Path,
    proposed :: Path, graph :: Graph) -> Boolean:
  doc:```consumes the current node, the stops left, the proposed path left
      and the graph; returns if this in line with the greedy algo```
  if stops-left.length() == 0:
    true
  else:
    dijk-res = dijkstra-helper(current, graph, [string-dict: current, 0], 
      [string-dict: current, [list: current]], insert(pp(current, 0, 
          [list: current]), mt))
    minimum-dist = L.foldl(lam(acc, x): 
        if dijk-res.dist.get-value(x) < acc:
          dijk-res.dist.get-value(x)
        else:
          acc
        end
      end, dijk-res.dist.get-value(stops-left.first), stops-left)
    all-minimal = L.filter({(x): dijk-res.dist.get-value(x) == minimum-dist},
      stops-left)

    if all-minimal.member(stops-left.first):
      proposed-to-next = list-up-to-elt(proposed, stops-left.first)
      if length-path(proposed-to-next, graph) == minimum-dist:
        is-valid-tour-on-path(stops-left.first, stops-left.rest, 
          proposed.drop(proposed-to-next.length() - 1), graph)
      else:
        false
      end
    else:
      false
    end
  end
where:
  is-valid-tour-on-path("3", [list: "1", "4"], [list: "3", "1", "2", "4"], g) 
    is true
  is-valid-tour-on-path("1", [list: "4"], [list: "1", "2", "4"], g) 
    is true
  is-valid-tour-on-path("1", [list: "4"], [list: "2", "4"], g) 
    is false
end


fun list-up-to-elt<A>(list-ext :: List<A>, elt :: A) -> List<A>:
  doc:```given a list and an elt; returns the list up to and including elt```
  cases (List) list-ext:
    | empty => empty
    | link(f, r) =>
      if f == elt:
        [list: elt]
      else:
        link(f, list-up-to-elt(r, elt))
      end
  end
where:
  list-up-to-elt(empty, 1) is empty
  list-up-to-elt([list: 1, 2, 3, 4, 5, 6], 4) is [list: 1, 2, 3, 4]
end

Week 10 Accelerated Intro to CS

Lecture 23 Detecting Cycles w/union-find

Watching lecture Wed 10/31/18 we skipped lecture 22 which is an internal Brown specific lecture about which classes you could take after this one.

If you can't see the function contracts what he's written on the board @03:54

Sets
unions :: S x S ->
are-in-same-set :: elt x elt -> Boolean

Where x is defined as the cross product, the set of all possible ordered combinations consisting of members in both sets like (A, B) of type A x B . Prof K informs us this is how type inference works in many well known compilers. @33:00 he's trying to point out that we have a graph data structure, and this data structure of sets is totally seperate, an auxilliary one to help us find the minimum spanning tree using sets. @40:10 we are now learning mutation as our programs need to have a notion of time. "I just made any parallel programs you write for the rest of your life hell".

Lecture 24 union-find II

Watching lecture Fri 11/2/18. Try the code yourself:

data Box: | box(ref v) end
b = box(0)
the-val = b!v 
the-ref = b.v
b!v{v: 1}
# the-ref is now 1 but the-val is still 0
the-new-val = b!v
# the-new-val is now 1

The difference is the-val and the-new-val went to a memory location and took the current value (a number) while the-ref took a kind of pointer to b.v and everytime you update b.v it now updates the-ref as well since they point to the same thing. He explains a bit of JQuery in this lecture. The last 20 mins or so of the lecture we can't see the board but he's talking about this chapter in Algorithms That Exploit State.

Ackermann's function is on wikipedia and massively grows \(2^{2^{2^{2^{...}}}}\) and the inverse Ackermann's function and the paper Prof K suggested we read is covered in the first lecture of Advanced Algorithms we do after this.

Assignment 13

MSTs assignment and template. Reminder from lecture 20/21 Kruskal's algorithm is take a sorted list of edges ascending in weight, choose the lightest edge, test it doesn't create a cycle if so add it to our set and repeat until there's no more edges left to test. Prim's solution is similar to Dijkstra's algorithm traversing the entire graph using greedy strategy always taking the lightest path and keeping a priority queue/checking for cycles before adding the lightest edge. Detecting cycles was then covered in lecture 23/24 and in the book as union find.

In the problem statement these are undirected edges so [list: edge("A", "B", 3), edge("B", "A", 3)] are both the same edge. Our task summarized is to implement another testing oracle that consumes 2 MST algorithms, generates random graphs for inputs and repeatedly tests if the MST algorithms are correct against each other's outputs as per the writeup. We can use satisfies and violates test operators.

Student solution:

provide *
provide-types *

include shared-gdrive("mst-support.arr", "14fGt9flbRDF3GKwiYWbIlet63fbwxczF")
include my-gdrive("mst-common.arr")

# DO NOT CHANGE ANYTHING ABOVE THIS LINE
#
# You may write implementation-specific tests (e.g., of helper functions)
# in this file.

import lists as L
import string-dict as SD

### Test Union Find
check "fynd":
  fynd(elt(0, some(elt(3, none)))) is=~ elt(3, none)
  fynd(elt(0, none)) is=~ elt(0, none)
end

check "union":
  elts = map(elt(_, none), range(0,4))
  union(elts.get(0), elts.get(3))
  elts is=~ [list: elt(0, some(elt(3, none))), elt(1, none), 
    elt(2, none), elt(3, none)]
  union(elts.get(0), elts.get(0)) 
  elts is=~ [list: elt(0, some(elt(3, none))), 
    elt(1, none), elt(2, none), elt(3, none)]
end

### Heaps Implementation Based on Heap Lab :)
data Heap:
  | mt
  | node(value :: Edge, left :: Heap, right :: Heap)
end

test-heap = node(edge("a", "b", 1), node(edge("c", "d", 3), 
    node(edge("e", "f", 4), mt, mt), node(edge("g", "h", 5), mt, mt)),
  node(edge("c", "d", 6), node(edge("e", "f", 7), mt, mt), 
    node(edge("g", "h", 8), mt, mt)))

fun insert(el :: Edge, h :: Heap) -> Heap:
  doc: ```Takes in a PossiblePath elt and a proper Heap h produces
       a proper Heap that has all of the elements of h and elt.```
  cases (Heap) h:
    | mt => node(el, mt, mt)
    | node(v,l,r) =>
      if el.weight > v.weight:
        node(v, insert(el, r), l)
      else:
        node(el, insert(v, r), l)
      end
  end
where:
  insert(edge("a", "b", 1), mt) is node(edge("a", "b", 1), mt, mt)
  insert(edge("z", "x", 20), test-heap) is node(edge("a", "b", 1), 
    node(edge("c", "d", 6), node(edge("g", "h", 8), node(edge("z", "x", 20), 
          mt, mt), mt), node(edge("e", "f", 7), mt, mt)), 
    node(edge("c", "d", 3), node(edge("e", "f", 4), mt, mt), node(edge("g", "h",
          5), mt, mt)))
  insert(edge("A", "B", 0), node(edge("a", "b", 1), mt, mt)) is node(edge("A", 
      "B", 0), node(edge("a", "b", 1), mt, mt), mt)
end

fun remove-min(h :: Heap%(is-node)) -> Heap:
  doc: ```Given a proper, non-empty Heap h, removes its minimum element.```
  amputated = amputate-bottom-left(h)
  cases (Heap) amputated.heap:
    | mt => mt
    | node(v,l,r) =>
      reorder(rebalance(node(amputated.elt, l, r)))
  end
where:
  remove-min(test-heap) is node(edge("c", "d", 3), node(edge("c", "d", 6), 
      node(edge("e", "f", 7), mt, mt), node(edge("g", "h", 8), mt, mt)), 
    node(edge("e", "f", 4), node(edge("g", "h", 5), mt, mt), mt))
end

fun rebalance(h :: Heap) -> Heap:
  doc: ```Given a Heap h, switches all children along the leftmost path```
  cases (Heap) h:
    | mt => mt
    | node(v,l,r) => node(v,r,rebalance(l))
  end
where:
  rebalance(node(edge("c", "d", 3), node(edge("c", "d", 6), 
        node(edge("e", "f", 7), mt, mt), mt), mt))
    is node(edge("c", "d", 3), mt, node(edge("c", "d", 6), mt, 
      node(edge("e", "f", 7), mt, mt)))
  rebalance(node(edge("c", "d", 3), mt, node(edge("c", "d", 6), mt, 
        node(edge("e", "f", 7), mt, mt))))
    is node(edge("c", "d", 3), node(edge("c", "d", 6), mt, 
      node(edge("e", "f", 7), mt, mt)), mt)
end

fun get-min(h :: Heap%(is-node)) -> Edge:
  doc: ```Takes in a proper, non-empty Heap h and produces the
       minimum weight in h.```
  cases (Heap) h:
    | mt => raise("Invalid input: empty heap")
    | node(val,_,_) => val
  end
where:
  get-min(test-heap) is edge("a", "b", 1)
end

data Amputated:
  | elt-and-heap(elt :: Edge, heap :: Heap)
end

fun amputate-bottom-left(h :: Heap%(is-node)) -> Amputated:
  doc: ```Given a Heap h, produes an Amputated that contains the
       bottom-left element of h, and h with the bottom-left element removed.```
  cases (Heap) h:
    | mt => raise("Invalid input: empty heap")
    | node(value, left, right) =>
      cases (Heap) left:
        | mt => elt-and-heap(value, mt)
        | node(_, _, _) =>
          rec-amputated = amputate-bottom-left(left)
          elt-and-heap(rec-amputated.elt,
            node(value, rec-amputated.heap, right))
      end
  end
where:
  amputate-bottom-left(test-heap) is elt-and-heap(edge("e", "f", 4), 
    node(edge("a", "b", 1), node(edge("c", "d", 3), mt, node(edge("g", "h", 5), 
          mt, mt)), node(edge("c", "d", 6), node(edge("e", "f", 7), mt, mt), 
        node(edge("g", "h", 8), mt, mt))))
end

fun reorder(h :: Heap) -> Heap:
  doc: ```Given a Heap h, where only the top node is misplaced,
       produces a Heap with the same elements but in proper order.```
  cases(Heap) h:
    | mt => mt
    | node(val, lh, rh) =>
      cases(Heap) lh:
        | mt => node(val, mt, mt)
        | node(lval, llh, lrh) =>
          cases(Heap) rh:
            | mt =>
              if val.weight < lval.weight:
                node(val, node(lval, mt, mt), mt)
              else:
                node(lval, node(val, mt, mt), mt)
              end
            | node(rval, rlh, rrh) =>
              if lval.weight <= rval.weight:
                if val.weight <= lval.weight:
                  h
                else:
                  node(lval, reorder(node(val, llh, lrh)), rh)
                end
              else:
                if val.weight <= rval.weight:
                  h
                else:
                  node(rval, lh, reorder(node(val, rlh, rrh)))
                end
              end
          end
      end
  end
where:
  reorder(mt) is mt
  reorder(test-heap) is test-heap
  reorder(node(edge("a", "b", 20), node(edge("c", "d", 3), 
        node(edge("e", "f", 4), mt, mt), node(edge("g", "h", 5), mt, mt)),
      node(edge("c", "d", 6), node(edge("e", "f", 7), mt, mt), 
        node(edge("g", "h", 8), mt, mt)))) is node(edge("c", "d", 3), 
    node(edge("e", "f", 4), node(edge("a", "b", 20), mt, mt), 
      node(edge("g", "h", 5), mt, mt)), node(edge("c", "d", 6), 
      node(edge("e", "f", 7), mt, mt), node(edge("g", "h", 8), mt, mt)))
end

### Helper Functions ###
fun adj-list-build(graph :: Graph) -> StringDict<List<Edge>>:
  doc:```consumes a graph; returns a string dictionary with nodes as keys
      and an adjacency list as values```
  for fold(adj-list from mt-dict, curr-edge from graph):
    so-far-a = adj-list.get(curr-edge.a)
    so-far-b = adj-list.get(curr-edge.b)
    ask:
      | (so-far-a == none) and (so-far-b == none) then:
        adj-list.set(curr-edge.a, [list: curr-edge]).set(curr-edge.b, 
          [list: curr-edge])
      | so-far-a == none then:
        adj-list.set(curr-edge.a, [list: curr-edge]).set(curr-edge.b, 
          link(curr-edge, so-far-b.value))
      | so-far-b == none then:
        adj-list.set(curr-edge.b, [list: curr-edge]).set(curr-edge.a, 
          link(curr-edge, so-far-a.value))
      | otherwise:
        adj-list.set(curr-edge.a, link(curr-edge, so-far-a.value)).set(
          curr-edge.b, link(curr-edge, so-far-b.value))
    end
  end
where:
  adj-list-build(empty) is mt-dict
  adj-list-build([list: edge("a", "b", 10), edge("a", "b", 3), 
      edge("c", "a", 3)])
    is [string-dict: "a", [list: edge("c", "a", 3), edge("a", "b", 3), 
      edge("a", "b", 10)], "b", [list: edge("a", "b", 3), edge("a", "b", 10)], 
    "c", [list: edge("c", "a", 3)]]
  adj-list-build([list: edge("a", "b", 10), edge("a", "b", -2)]) 
    is [string-dict: "a", [list: edge("a", "b", -2), edge("a", "b", 10)], "b", 
    [list: edge("a", "b", -2), edge("a", "b", 10)]]
end

fun prim-helper(building :: List<Edge>, queue :: Heap, solution :: Set<String>,
    adj-list :: StringDict<Edge>) -> List<Edge>:
  doc:```consumes a building solution, solution so far, adjacency list, and a 
      queue; returns the edges in the MST```
  cases (Heap) queue:
    | mt => building
    | node(_, _, _) =>
      next-edge = get-min(queue)
      if solution.member(next-edge.a) and solution.member(next-edge.b):
        prim-helper(building, remove-min(queue), solution, adj-list)
      else if solution.member(next-edge.a):
        new-queue = fold({(acc, x): insert(x, acc)}, remove-min(queue), 
          adj-list.get-value(next-edge.b))
        prim-helper(link(next-edge, building), new-queue, 
          solution.add(next-edge.b), adj-list)
      else:
        new-queue = fold({(acc, x): insert(x, acc)}, remove-min(queue), 
          adj-list.get-value(next-edge.a))
        prim-helper(link(next-edge, building), new-queue, 
          solution.add(next-edge.a), adj-list)
      end
  end
where:
  prim-helper(empty, node(edge("a", "b", 1), mt, mt), [set: "a"], 
    adj-list-build([list: edge("a", "b", 1)])) is [list: edge("a", "b", 1)]
  prim-helper(empty, node(edge("a", "b", -2), node(edge("a", "b", 10), mt, mt), 
      mt), [set: "a"], adj-list-build([list: edge("a", "b", 10), edge("a", 
          "b", -2)])) is [list: edge("a", "b", -2)]
end

fun graph-builder(to-add :: List<String>, solution :: List<String>) -> Graph:
  doc:```consumes nodes to add, and nodes already added; returns a random graph 
      with all nodes```
  cases (List) to-add:
    | empty => empty
    | link(f, r) =>
      sol-size = solution.length()
      edges = for fold(b from empty, idx from L.shuffle(range(0, num-random(
                sol-size) + 1))):
        for fold(build from b, idx2 from range(0, num-random(4) + 1)):
          link(edge(f, solution.get(idx), num-random(11) - 5), build)
        end
      end
      edges.append(graph-builder(r, link(f, solution)))
  end
where:
  graph-builder([list: "b", "c", "d"], [list: "a"]) 
    satisfies {(x): x.length() <= 18}
  graph-builder([list: "b", "c", "d"], [list: "a"]) 
    satisfies {(x): x.length() >= 3}
end

check "find-nodes":
  find-nodes(empty) is empty
  find-nodes([list: edge("a", "b", 1), edge("a", "b", 3), edge("b", "c", 4)])
    is [list: "c", "a", "b"]
end

### Main Functions ###

fun mst-kruskal(graph :: Graph) -> List<Edge>:
  doc:```consumes a graph; returns a minimal spanning tree using 
      kruskals algorithm```
  sorted-edges = graph.sort-by({(x, y): x.weight < y.weight}, 
    {(x, y): x.weight == y.weight})
  unique-nodes = find-nodes(sorted-edges)
  groups = fold({(acc, x): acc.set(x, elt(x, none))}, mt-dict, unique-nodes)
  for fold(edges from empty, curr-edge from sorted-edges):
    e1 = fynd(groups.get-value(curr-edge.a))
    e2 = fynd(groups.get-value(curr-edge.b))
    if not(e1 == e2):
      block:
        union(e1, e2)
        link(curr-edge, edges)
      end
    else:
      edges
    end
  end
end

fun mst-prim(graph :: Graph) -> List<Edge>:
  doc:```consumes a graph; returns a minimal spanning tree using
      prims algorithm```
  unique-nodes = L.distinct(fold({(acc, x): link(x.a, link(x.b, acc))}, empty,
      graph))
  adj-list = adj-list-build(graph)
  if graph == empty:
    empty
  else:
    queue = fold({(acc, x): insert(x, acc)}, mt, 
      adj-list.get-value(unique-nodes.first))
    prim-helper(empty, queue, [tree-set: unique-nodes.first], adj-list)
  end
end

fun generate-input(num-vertices :: Number) -> Graph:
  doc:```consumes a number; returns a random graph with that many vertices```
  if [set: 0, 1].member(num-vertices):
    empty
  else:
    nodes = map(string-from-code-point, range(97, 97 + num-vertices))
    graph-builder(nodes.rest, [list: nodes.first])
  end
end

fun mst-cmp(
    graph :: Graph,
    mst-alg-a :: (Graph -> List<Edge>),
    mst-alg-b :: (Graph -> List<Edge>))
  -> Boolean:
  doc:```consumes two mst algorithms and determines if they both produce a valid
      mst result```
  tree1 = mst-alg-a(graph)
  tree2 = mst-alg-b(graph)
  if ((tree1 == empty) and (tree2 == empty)) and (graph == empty):
    true
  else:
    graph-nodes = find-nodes(graph).sort()
    # check spanning as having all nodes as graph in solution
    spanning = (find-nodes(tree1).sort() == graph-nodes) and (find-nodes(
        tree2).sort() == graph-nodes)

    # is-tree by measuring number of edges is one less total nodes 
    tree = (tree1.length() == (graph-nodes.length() - 1)) and (tree2.
      length() == (graph-nodes.length() - 1))

    # calculate weight by summing weights over all edges and outputing result
    weight-calc = lam(x :: Graph): fold({(acc, y): acc + y.weight}, 0, x) end
    weight = (weight-calc(tree1) == weight-calc(tree2))

    # check all tree edges are members of the graph
    edges-cont = lam(x :: Graph): L.all(graph.member(_), x) end
    edges = (edges-cont(tree1) and edges-cont(tree2))

    edges and (weight and (tree and spanning))
  end
end

fun sort-o-cle(
    mst-alg-a :: (Graph -> List<Edge>),
    mst-alg-b :: (Graph -> List<Edge>))
  -> Boolean:
  doc:```consumes two mst algorithms and determines if they are valid
      by checking if their results ober a large sample space are equal```
  random-suite = map({(_): generate-input(num-random(15))}, range(0, 30))
  rand-res = L.all(mst-cmp(_, mst-alg-a, mst-alg-b), random-suite)
  
  edge-suite = [list: two-e-two-n, tree-graph, pos-sym-cycle, neg-sym-cycle, 
    pos-cycle, neg-cycle, disj-two-mult, disj-two-back, two-cycle-inter, 
    two-cycle-inter-amb, single-edge, two-e-two-n-same, empty]
  edge-res = L.all(mst-cmp(_, mst-alg-a, mst-alg-b), edge-suite)
  
  rand-res and edge-res
end

Their 'common code'

provide *
provide-types *

include shared-gdrive("mst-support.arr", "14fGt9flbRDF3GKwiYWbIlet63fbwxczF")

# DO NOT CHANGE ANYTHING ABOVE THIS LINE
#
# Write data bindings here that you'll need for tests in both mst-code.arr
# and mst-tests.arr

import lists as L
import string-dict as SD
type StringDict = SD.StringDict
string-dict = SD.string-dict

mt-dict = [string-dict: ]

### Union Find ###
data Elt: elt(v, ref parent :: Option<Elt>) end

fun fynd(e :: Elt) -> Elt:
  doc:```finds the source elt of an elt```
  cases (Option) e!parent:
    | none => e
    | some(p) =>
      block:
        new-parent = fynd(p)
        e!{parent: some(new-parent)}
        new-parent
      end
  end
end

fun union(e1 :: Elt, e2 :: Elt):
  doc:```unions two sets together in place```
  e1n = fynd(e1)
  e2n = fynd(e2)
  when not(e1 <=> e2):
    e1n!{parent: some(e2n)}
  end
end

### find nodes helper

fun find-nodes(graph :: Graph) -> List<String>:
  doc:```consumes a graph; returns the unique nodes it has```
  L.distinct(fold({(acc, x): link(x.a, link(x.b, acc))}, empty,
      graph))
end

### TESTING GRAPHS

## Lst same els implementation from map reduce. 
# Tests removed as they break exemplar

# count
fun count<A>(target :: A, a :: List<A>, eq-checker :: (A, A -> Boolean))
  -> Number:
  doc: "counts quantity of target in a"
  fun el-checker(el, cnt):
    if eq-checker(el, target):
      cnt + 1
    else:
      cnt
    end
  end
  a.foldl(el-checker, 0)
end

# lst-same-els
fun lst-same-els<A>(a :: List<A>,b :: List<A>, eq-checker :: (A, A -> Boolean))
  -> Boolean:
  doc: "checks whether two lists have the same elements in the same quantity"
  fun same-count(el, acc):
    acc and (count(el, a, eq-checker) == count(el, b, eq-checker))
  end
  (a.length() == b.length()) and a.foldl(same-count, true)
end

## Special Graphs

# two edges two nodes
two-e-two-n = [list: edge("a", "b", 1), edge("a", "b", 3)]

# already a tree
tree-graph = [list: edge("a", "b", 1), edge("b", "c", 2), edge("a", "d", 1)]

# symmetric cycle
pos-sym-cycle = [list: edge("a", "b", 2), edge("b", "c", 2), edge("c", "d", 2), 
  edge("d", "a", 2)]

# symmetric neg cycle
neg-sym-cycle = [list: edge("a", "b", -2), edge("b", "c", -2), 
  edge("c", "d", -2), edge("d", "a", -2)]

# non-sym cycle
pos-cycle = [list: edge("a", "b", 2), edge("b", "c", 3), edge("c", "d", 2), 
  edge("d", "a", 2)]

# negative non-sym cycle
neg-cycle = [list: edge("a", "b", -2), edge("b", "c", -3), edge("c", "d", -4), 
  edge("d", "a", -5)]

# disjoint two multiple possiblities 
disj-two-mult = [list: edge("a", "b", 2), edge("a", "c", 2), edge("b", "d", 2),
  edge("c", "d", 2), edge("d", "e", 2), edge("d", "f", 2), edge("e", "g", 2),
  edge("f", "g", 2)]

# disjoint approach from back
disj-two-back = [list: edge("a", "b", 3), edge("a", "c", 3), edge("b", "d", 2),
  edge("c", "d", 2), edge("d", "e", 3), edge("d", "f", 3), edge("e", "g", 2),
  edge("f", "g", 2)]

# interpolate two cycles
two-cycle-inter = [list: edge("a", "b", 2), edge("a", "b", -1), 
  edge("b", "c", 2), edge("b", "c", 3), edge("c", "a", 4), edge("c", "a", 6)]

# interpolate two cycles ambiguous
two-cycle-inter-amb = [list: edge("a", "b", 2), edge("a", "b", -1), 
  edge("b", "c", 2), edge("b", "c", 3), edge("c", "a", 2), edge("c", "a", 2)]

# single edge
single-edge = [list: edge("a", "b", 0)]

# two edges two nodes same weight
two-e-two-n-same = [list: edge("a", "b", 1), edge("a", "b", 3)]


### BAD MST IMPLEMENTATIONS

# empty returner
empty-mst = {(x): empty}

# return first element
fun first-mst(graph :: Graph) -> List<Edge>:
  doc:```returns first edge if it exists```
  cases (List) graph:
    | empty => empty
    | link(f, r) => [list: f]
  end
end

# return random selection
fun random-mst(graph :: Graph) -> List<Edge>:
  doc:```returns random subset of edges```
  g-size = graph.length()
  L.shuffle(graph).take(num-random(g-size))
end

# return n-1 lowest edges
fun naive-kruskal-mst(graph :: Graph) -> List<Edge>:
  doc: ```returns the n-1 lowest edge weights```
  cases (List) graph:
    | empty => empty
    | link(f, r) =>
      g-size = graph.length()
      graph.sort-by({(x, y): x.weight < y.weight}, {(x, y): x.weight == 
          y.weight}).take(g-size - 1)
  end
end

# labeling flipped
fun flipped-kruskal-mst(graph :: Graph) -> List<Edge>:
  doc:```consumes a graph; returns a minimal spanning tree using 
      kruskals algorithm but reverses edges labelling```
  sorted-edges = graph.sort-by({(x, y): x.weight < y.weight}, 
    {(x, y): x.weight == y.weight})
  unique-nodes = find-nodes(sorted-edges)
  groups = fold({(acc, x): acc.set(x, elt(x, none))}, mt-dict, unique-nodes)
  for fold(edges from empty, curr-edge from sorted-edges):
    e1 = fynd(groups.get-value(curr-edge.a))
    e2 = fynd(groups.get-value(curr-edge.b))
    if not(e1 == e2):
      block:
        union(e1, e2)
        link(edge(curr-edge.b, curr-edge.a, curr-edge.weight), edges)
      end
    else:
      edges
    end
  end
end

Their tests

include shared-gdrive("mst-support.arr", "14fGt9flbRDF3GKwiYWbIlet63fbwxczF")
include my-gdrive("mst-code.arr")
include my-gdrive("mst-common.arr")

# DO NOT CHANGE ANYTHING ABOVE THIS LINE
#
# Write your examples and tests in here. These should not be tests of
# implementation-specific details (e.g., helper functions).

import lists as L

# equality operator
eq = lam(x,y): x == y end

### MST KRUSKAL

check "mst-kruskal base case sensitivity":
  # empty test
  mst-kruskal(empty) is empty

  # singular edge
  mst-kruskal(single-edge) is single-edge
end

check "mst-kruskal multiple edge sensitivity":
  # two nodes two edges
  mst-kruskal(two-e-two-n) is [list: edge("a", "b", 1)]

  # two edges two nodes same weight
  mst-kruskal(two-e-two-n-same) is [list: edge("a", "b", 1)]

  # traverse interpolated cycles
  mst-kruskal(two-cycle-inter) satisfies lst-same-els(_, [list: edge("b", "c", 
        2), edge("a", "b", -1)], eq)
end

check "mst-kruskal properly traverses":
  # traverse a tree
  mst-kruskal(tree-graph) satisfies lst-same-els(_, tree-graph, eq)

  # traverse non-sym cycle
  mst-kruskal(pos-cycle) satisfies lst-same-els(_, [list: edge("a", "b", 2),
      edge("c", "d", 2), edge("d", "a", 2)], eq)

  # traverse non-sym negative cycle
  mst-kruskal(neg-cycle) satisfies lst-same-els(_, [list: edge("b", "c", -3), 
      edge("c", "d", -4), edge("d", "a", -5)], eq)
end

check "mst-kruskal ambiguous manually checked":
  # symmetric cycle
  mst-kruskal(pos-sym-cycle) satisfies {(x): L.any(lam(y): lst-same-els(x, y, 
        eq) end, [list: [list: edge("a", "b", 2), edge("b", "c", 2), 
          edge("c", "d", 2)],[list: edge("b", "c", 2), edge("c", "d", 2), 
          edge("d", "a", 2)]])}

  # symmetric neg cycle
  mst-kruskal(neg-sym-cycle) satisfies {(x): L.any(lam(y): lst-same-els(x, y, 
        eq) end, [list: [list: edge("a", "b", -2), edge("b", "c", -2), 
          edge("c", "d", -2)],[list: edge("b", "c", -2), edge("c", "d", -2), 
          edge("d", "a", -2)]])}

  # disjoint two possibilities approach back
  mst-kruskal(disj-two-back) satisfies {(x): L.any(lam(y): lst-same-els(x, y, 
        eq) end, [list: [list: edge("a", "b", 3), edge("b", "d", 2), 
          edge("c", "d", 2), edge("d", "e", 3), edge("e", "g", 2), 
          edge("f", "g", 2)], 
        [list: edge("a", "c", 3), edge("b", "d", 2), edge("c", "d", 2), 
          edge("d", "f", 3), edge("e", "g", 2), edge("f", "g", 2)],
        [list: edge("a", "b", 3), edge("b", "d", 2), edge("c", "d", 2),  
          edge("d", "f", 3), edge("e", "g", 2), edge("f", "g", 2)],
        [list: edge("a", "c", 3), edge("b", "d", 2), edge("c", "d", 2), 
          edge("d", "e", 3), edge("e", "g", 2), edge("f", "g", 2)]])}
end


### MST PRIM

check "mst-prim base case sensitivity":
  # empty test
  mst-prim(empty) is empty

  # singular edge
  mst-prim(single-edge) is single-edge
end

check "mst-prim multiple edge sensitivity":
  # two notes two edges
  mst-prim(two-e-two-n) is [list: edge("a", "b", 1)]

  # two edges two nodes same weight
  mst-prim(two-e-two-n-same) is [list: edge("a", "b", 1)]

  # traverse interpolated cycles
  mst-prim(two-cycle-inter) satisfies lst-same-els(_, [list: edge("b", "c", 
        2), edge("a", "b", -1)], eq)
end

check "mst-prim properly traverses":
  # traverse a tree
  mst-prim(tree-graph) satisfies lst-same-els(_, tree-graph, eq)

  # traverse non-sym cycle
  mst-prim(pos-cycle) satisfies lst-same-els(_, [list: edge("a", "b", 2),
      edge("c", "d", 2), edge("d", "a", 2)], eq)

  # traverse non-sym negative cycle
  mst-prim(neg-cycle) satisfies lst-same-els(_, [list: edge("b", "c", -3), 
      edge("c", "d", -4), edge("d", "a", -5)], eq)
end

check "mst-kruskal ambiguous manually checked":
  # symmetric cycle
  mst-prim(pos-sym-cycle) satisfies {(x): L.any(lam(y): lst-same-els(x, y, 
        eq) end, [list: [list: edge("a", "b", 2), edge("b", "c", 2), 
          edge("c", "d", 2)],[list: edge("b", "c", 2), edge("c", "d", 2), 
          edge("d", "a", 2)]])}

  # symmetric neg cycle
  mst-prim(neg-sym-cycle) satisfies {(x): L.any(lam(y): lst-same-els(x, y, 
        eq) end, [list: [list: edge("a", "b", -2), edge("b", "c", -2), 
          edge("c", "d", -2)],[list: edge("b", "c", -2), edge("c", "d", -2), 
          edge("d", "a", -2)]])}

  # disjoint two possibilities approach back
  mst-prim(disj-two-back) satisfies {(x): L.any(lam(y): lst-same-els(x, y, 
        eq) end, [list: [list: edge("a", "b", 3), edge("b", "d", 2), 
          edge("c", "d", 2), edge("d", "e", 3), edge("e", "g", 2), 
          edge("f", "g", 2)], 
        [list: edge("a", "c", 3), edge("b", "d", 2), edge("c", "d", 2), 
          edge("d", "f", 3), edge("e", "g", 2), edge("f", "g", 2)],
        [list: edge("a", "b", 3), edge("b", "d", 2), edge("c", "d", 2),  
          edge("d", "f", 3), edge("e", "g", 2), edge("f", "g", 2)],
        [list: edge("a", "c", 3), edge("b", "d", 2), edge("c", "d", 2), 
          edge("d", "e", 3), edge("e", "g", 2), edge("f", "g", 2)]])}
end


### GENERATE INPUT

check "generate-input base case":
  # empty graph 0
  generate-input(0) is empty

  # empty graph 1
  generate-input(1) is empty
end

check "generate-input size":
  # check over large space
  fold({(acc1, y): link(L.distinct(fold({(acc, x): link(x.a, link(x.b, acc))}, 
            empty, generate-input(y))).length(), acc1)}, empty, range(2, 30))
    is range(2, 30).reverse()
end

check "generate-input connectedness":
  # the connected condition means that there must be >= n - 1 unique edges
  input-g = map({(_): generate-input(num-random(15) + 15)}, range(0, 10))
  dist-ed = map({(y): L.distinct(map({(x): {x.a; x.b}}, y))}, input-g)
  dist-ed satisfies L.all({(y): y.length() >= (L.distinct(fold({(acc, x): 
              link(x.{0}, link(x.{1}, acc))}, empty, y)).length() - 1)}, _)
end

check "generate-input no loops":
  # check no a -> a
  input-g = map({(_): generate-input(num-random(15) + 15)}, range(0, 10))
  input-g satisfies L.all({(y): L.all(lam(x): not(x.a == x.b) end, y)}, _)
end

check "generate-input unidirectional":
  # check that if a -> b no b -> a
  input-g = map({(_): generate-input(num-random(15) + 15)}, range(0, 10))
  dist-ed = map({(y): L.distinct(map({(x): {x.a; x.b}}, y))}, input-g)
  dist-ed satisfies L.all({(y): L.all(lam(x): not(y.member({x.{1}; x.{0}})) end,
        y)}, _)
end


### MST CMP

check "mst-cmp base case":
  # empty graph correct implementations 
  empty satisfies mst-cmp(_, mst-prim, mst-kruskal)

  # empty graph false implementation but still correct
  empty satisfies mst-cmp(_, mst-prim, empty-mst)
end

check "mst-cmp correct implementations deterministic graphs":
  # two notes two edges
  two-e-two-n satisfies mst-cmp(_, mst-prim, mst-kruskal)

  # two edges two nodes same weight
  two-e-two-n-same satisfies mst-cmp(_, mst-prim, mst-kruskal)

  # traverse interpolated cycles
  two-cycle-inter satisfies mst-cmp(_, mst-prim, mst-kruskal)

  # traverse a tree
  tree-graph satisfies mst-cmp(_, mst-prim, mst-kruskal)

  # traverse non-sym cycle
  pos-cycle satisfies mst-cmp(_, mst-prim, mst-kruskal)

  # traverse non-sym negative cycle
  neg-cycle satisfies mst-cmp(_, mst-prim, mst-kruskal)
end

check "mst-cmp correct implementations non-deterministic":
  # positive symetric cycle
  pos-sym-cycle satisfies mst-cmp(_, mst-prim, mst-kruskal)

  # negative symetric cycle
  neg-sym-cycle satisfies mst-cmp(_, mst-prim, mst-kruskal)

  # disjoint two parts multiple paths
  disj-two-mult satisfies mst-cmp(_, mst-prim, mst-kruskal)

  # disjoint multiple paths with backtracking
  disj-two-back satisfies mst-cmp(_, mst-prim, mst-kruskal)

  # two cycles interpolated ambiguous
  two-cycle-inter-amb satisfies mst-cmp(_, mst-prim, mst-kruskal)
end

check "mst-cmp failing implementations but same output":
  # both return empty
  two-e-two-n violates mst-cmp(_, empty-mst, empty-mst)
  
  # both return one edge in the graph
  tree-graph violates {(x): mst-cmp(x, first-mst, first-mst)}
end

check "mst-cmp failing specific criteria":
  # makes sure mst-cmp can weed out random
  disj-two-mult violates {(x): L.all({(y): mst-cmp(x, random-mst, mst-prim)}, 
      range(0, 10))}
  
  # weights off
  pos-cycle violates mst-cmp(_, mst-kruskal, {(x): [list: edge("b", "c", 3), 
        edge("c", "d", 2), edge("d", "a", 2)]})
  
  # spanning off
  pos-sym-cycle violates mst-cmp(_, mst-kruskal, {(x): [list: edge("a", "b", 2),
        edge("b", "c", 2), edge("b", "c", 2)]})
  
  # tree off
  single-edge violates mst-cmp(_, mst-kruskal, {(x): [list: edge("a", "b", 0),
        edge("a", "b", 0)]})
  
  # inclusion off
  pos-cycle violates mst-cmp(_, mst-kruskal, {(x): [list: edge("b", "c", -5), 
        edge("c", "d", -5), edge("d", "a", 17)]})
end

### SORT-O-CLE

check "sort-o-cle both valid":
  # mst-prim and mst-kruskal should be correct
  sort-o-cle(mst-prim, mst-kruskal) is true
end

check "sort-o-cle one valid":
  # mst-prim up against only empty
  empty-mst violates sort-o-cle(_, mst-prim)
  
  # first element
  first-mst violates sort-o-cle(_, mst-kruskal)
  
  # random
  random-mst violates sort-o-cle(_, mst-prim)
  
  # naive-kruskal
  naive-kruskal-mst violates sort-o-cle(_, mst-kruskal)
  
  # flipped edge labeling
  flipped-kruskal-mst violates sort-o-cle(_, mst-kruskal)
end

check "sort-o-cle input two wrong":
  sort-o-cle(empty-mst, first-mst) is false
  
  sort-o-cle(first-mst, random-mst) is false
  
  sort-o-cle(naive-kruskal-mst, flipped-kruskal-mst) is false
end

Week 11 Accelerated Intro to CS

Lecture 25 Closures

Watching lecture Mon 11/5/18. You can't see what he's writing off screen to show why 'equals-never' is impossible, but it's the subject of the entire next lecture. Rest of this lecture is demonstrating how closures/language scoping rules work, and how they don't work as you expect in Javascript/Python so you need to read the language scoping rules and not make assumptions. He talks about CS173 which we are doing after this course.

Lecture 26 Halting problem

Watching lecture Wed 11/7/18 lecture an entertaining introduction to theoretical CS that expands on the previous lecture 'can equals-never exist' question.

Lecture 31 P = NP

I moved this up since it's also teaching theoretical CS.

Watch lecture Mon 11/19/18 this is a must watch lecture that explains non-determinism/theoretical complexity theory better than anywhere else using as examples all the oracles we wrote. If you like complexity theory see this channel or watch this lecture by Erik Demaine it's a graduate class on practical applications of reductions where he'll show you how most decision problems (something with a yes/no answer) are not solvable by algorithms. Complexity theory is wide open to discoveries.

If you've ever heard of regex or regular expressions they are explained here with DFAs.

Week 12 Accelerated Intro to CS

Lecture 27 & 28 Memoization

Watching lecture Fri 11/09/18 and lecture Mon 11/12/18 introduces typical assignment operators and var keywords you'd see in other languages. Probably the best intro to memoization you'll find, he describes it as a seperation of concerns, explains closures some more, explains lambdas some more, finally we learn memoize() is really an identity function: given a function it returns the same function.

Lecture 29 & 30 Dynamic Programming

Watching lecture Wed 11/14/18 and lecture Fri 11/16/18. Dynamic programming is the bottom up approach to problem solving and memoization is the top down approach hilariously explained as 'people were afraid of recursion so invented DP'. The book covers all this in detail. If space is a concern, a memoized version is still useful as it can be used for testing against your dynamic programming implementation.

Final assignment

Fluid Images assignment here. Cut+Paste the following to access the starter code functions in the writeup like image-data-to-image:

provide *

include shared-gdrive("fluid-images-definitions.arr", "1qYtcBi1vQUhOZSgCyqLGv8GTJaWxypaL")

import image-url,
image-width,
image-height,
image-to-color-list,
color-list-to-image
from image

import image-structs as I

fun run-on-url(url :: String, func :: (Image, Number -> Image), n :: Number):
  doc: ```Runs one of your Fluid Images implementations on an image
       downloaded from a url```
  fl-image :: Image =
    let raw-image = image-url(url),
      width       = image-width(raw-image),
      height      = image-height(raw-image),
      image-data  = image-to-color-list(raw-image):
      image-data.map({(c):color(c.red, c.green, c.blue)})
        ^ builtins.raw-array-from-list
        ^ image(width,height).make
    end
  liquified = func(fl-image, n)
  im-list = lists.fold({(acc, cur): acc.append(cur)}, empty, liquified.pixels)
  im-list-colors = im-list.map({(e): I.color(e.red, e.green, e.blue, 1)})
  color-list-to-image(im-list-colors, liquified.width, liquified.height, 0, 0)
end

Student solution to compare to your own, I include the real student solutions I found because by this point in the course they have been drilled in testing by prof K so you may want to see what a real solution would look like and not my own hacked together solution as I was not a thorough as these students. Reminder you have to use the above code to import the shared gdrve:

import string-dict as SD
type StringDict = SD.StringDict
string-dict = SD.string-dict

### DATA DEFINITIONS
data SearchRet: ret(weight :: Number, path :: List<Number>) end
data Mem: mem(in :: List<List<Number>>, out :: List<List<SearchRet>>) end

### HELPER FUNCTIONS
fun calculate-brightness(in :: Color) -> Number:
  doc:```computes the brightness of a color```
  in.red + in.green + in.blue
where:
  calculate-brightness(color(1, 2, 3)) is 6
  calculate-brightness(color(0, 0, 0)) is 0
  calculate-brightness(color(34, 24, 90)) is 148
end

fun image-brightness(im :: Image) -> List<List<Number>>:
  doc:```computes the brightness of an entire image```
  pixels = im.pixels
  height = im.height
  width = im.width
  for fold(x-arr from empty, row from range(0, height + 2)):
    link(
      for fold(y-arr from empty, col from range(0, width + 2)):
        ask:
          | (row == 0) or (row == (height + 1)) then:
            link(0, y-arr)
          | (col == 0) or (col == (width + 1)) then:
            link(0, y-arr)
          | otherwise:
            link(calculate-brightness(pixels.get(row - 1).get(col -  1)), 
              y-arr)
        end
      end.reverse(), x-arr)
  end.reverse()
where:
  image-brightness([image(3,2): color(0,2,0), color(0,0,0),  color(0,3,0), 
      color(1,0,0), color(0,3,0),  color(0,0,4)])
    is [list: [list: 0, 0, 0, 0, 0], [list: 0, 2, 0, 3, 0], [list: 0, 1, 3, 4, 
      0], [list: 0, 0, 0, 0, 0]]
  image-brightness([image(1,1): color(133, 1, 45)]) is [list: [list: 0, 0, 0], 
    [list: 0, 179, 0], [list: 0, 0, 0]]
  image-brightness(image6) is [list: [list: 0, 0, 0, 0, 0, 0], [list: 0, 1, 9, 
      3, 6, 0], [list: 0, 0, 0, 0, 0, 0]]
end

fun energy(im :: Image) -> List<List<Number>>:
  doc:```computes the energy of each element```
  bright = image-brightness(im)
  height = im.height
  width = im.width
  for fold(x-arr from empty, row from range(1, height + 1)):
    link(
      for fold(y-arr from empty, col from range(1, width + 1)):
        x-energy = (((((bright.get(row - 1).get(col - 1) + (2 * bright.get(row)
                      .get(col - 1))) + bright.get(row + 1).get(col - 1)) - 
              bright.get(row - 1).get(col + 1)) - (2 * bright.get(row).get(col 
                  + 1))) - bright.get(row + 1).get(col + 1))

        y-energy = (((((bright.get(row - 1).get(col - 1) + (2 * bright.get(row 
                        - 1).get(col))) + bright.get(row - 1).get(col + 1)) - 
              bright.get(row + 1).get(col - 1)) - (2 * bright.get(row + 1).
              get(col))) - bright.get(row + 1).get(col + 1))

        link(num-sqrt(num-sqr(x-energy) + num-sqr(y-energy)), y-arr)
      end.reverse(), x-arr)
  end.reverse()
where:
  energy([image(3,2): color(0,2,0), color(0,0,0),  color(0,3,0), 
      color(1,0,0), color(0,3,0),  color(0,0,4)]) 
    is%(within(0.001)) [list: [list: ~5.83095, ~12.08304, ~11.40175], [list: 
      ~7.21110, ~8.60232, ~8.48528]]
  energy([image(1,1): color(133, 1, 45)]) is%(within(0.001)) [list: [list: 0]]
  energy([image(2, 2): color(1,1,1), color(1,0,2), color(2,0,1), color(0,3,0)])
    is%(within(0.0001)) [list: [list: ~12.72792, ~12.72792], [list: ~12.72792, 
      ~12.72792]]
  energy(rand-image) 
    is%(within(0.0001)) [list: [list: ~1676.70450, ~2084.75082, ~2125.71493, 
        ~1962.29457], [list: ~2162.65068, ~704.82480, ~178.31432, ~1656.33631], 
      [list: ~1940.31801, ~1988.38879, ~2120.82436, ~1801.16073]]
end

# cannot test this function
fun memoize(liquify :: (List<List<Number>> -> List<List<SearchRet>>)) 
  -> (List<List<Number>> -> List<List<SearchRet>>):
  doc:```consumes a liquifier and memoizes it```
  var memory = empty
  lam(compute :: List<List<Number>>) block:
    thresh = within(0.0001)
    re = find({(m :: Mem): thresh(m.in, compute)}, memory)
    cases (Option) re block:
      | some(v) => v.out
      | none =>
        actual = liquify(compute)
        memory := link(mem(compute, actual), memory)
        actual
    end
  end
end

rec seam :: (List<List<Number>> -> List<List<SearchRet>>) = 
  #consumes a list of a list of energies and returns the searchresults
  memoize(
    lam(compute :: List<List<Number>>):
      cases (List) compute:
        | empty => empty
        | link(f, r) =>
          before = seam(r)
          addition = for fold(acc from empty, elt from range(0, f.length())):
            ask:
              | before == empty then:
                link(ret(f.get(elt), [list: elt]), acc)
              | elt == 0 then:
                min-weight = fold({(acc1, x): if x.weight < acc1.weight: x 
                    else: acc1 end}, ret(100000000, empty), [list: before.first
                      .get(elt), before.first.get(elt + 1)])
                link(ret(f.get(elt) + min-weight.weight, link(elt, 
                      min-weight.path)), acc)
              | elt == (f.length() - 1) then:
                min-weight = fold({(acc1, x): if x.weight < acc1.weight: x 
                    else: acc1 end}, ret(100000000, empty), [list: before.first
                      .get(f.length() - 2), before.first.get(f.length() - 1)])
                link(ret(f.get(elt) + min-weight.weight, link(elt, 
                      min-weight.path)), acc)
              | otherwise:
                min-weight = fold({(acc1, x): if x.weight < acc1.weight: x 
                    else: acc1 end}, ret(100000000, empty), [list: before.first
                      .get(elt - 1), before.first.get(elt), before.first
                      .get(elt + 1)])
                link(ret(f.get(elt) + min-weight.weight, link(elt, 
                      min-weight.path)), acc)
            end
          end.reverse()
          link(addition, before)
      end
    end)

check "seam":
  seam(energy([image(3,2): color(0,2,0), color(0,0,0),  color(0,3,0), 
        color(1,0,0), color(0,3,0),  color(0,0,4)]))
    is%(within(0.0001)) [list: [list: ret(~13.04205, [list: 0, 0]), 
      ret(~19.29414, [list: 1, 0]), ret(~19.88703, [list: 2, 2])], [list: 
      ret(~7.21110, [list: 0]), ret(~8.60232, [list: 1]), ret(~8.48528, [list: 
          2])]]
  seam(energy([image(4, 3): color(100, 46, 1), color(148, 60, 109), color(210, 
          10, 153), color(114, 161, 217), color(115, 68, 96), color(235, 211, 
          143), color(249, 212, 51), color(172, 116, 209), color(180, 217, 96), 
        color(61, 243, 184), color(124, 38, 76), color(95, 83, 249)]))
    is%(within(0.0001)) [list: [list: ret(~4321.84732, [list: 0, 1, 0]), 
      ret(~4064.22589, [list: 1, 2, 3]), ret(~4105.19000, [list: 2, 2, 3]), 
      ret(~3941.76964, [list: 3, 2, 3])], [list: ret(~4102.96870, [list: 0, 0]),
      ret(~2645.14281, [list: 1, 0]), ret(~1979.47506, [list: 2, 3]), 
      ret(~3457.49705, [list: 3, 3])], [list: ret(~1940.31801, [list: 0]), 
      ret(~1988.38879, [list: 1]), ret(~2120.82436, [list: 2]), ret(~1801.16073,
        [list: 3])]]
  seam(energy([image(2, 2): color(1,1,1), color(1,0,2), color(2,0,1), 
        color(0,3,0)])) 
    is%(within(0.001)) [list: [list: ret(~25.45584, [list: 0, 0]), 
      ret(~25.45584, [list: 1, 0])], [list: ret(~12.72792, [list: 0]), 
      ret(~12.72792, [list: 1])]]
end

fun seam-DP(input :: Image) -> Image block:
  doc:```consumes an image and returns the lowest seam removed```
  height = input.height
  width = input.width
  paths = build-array({(_): array-of(none, width)}, height)
  energized = energy(input)

  for each(col from range(0, width)):
    paths.get-now(0).set-now(col, some(ret(energized.get(0).get(col), [list: 
            col])))
  end

  for each(row from range(1, height)):
    for each(col from range(0, width)):
      ask:
        | col == 0 then:
          min-weight = fold({(acc1, x): if x.weight < acc1.weight: x 
              else: acc1 end}, ret(100000000, empty), [list: paths.get-now(row 
                  - 1).get-now(col).value,  paths.get-now(row - 1).get-now(col 
                  + 1).value])
          paths.get-now(row).set-now(col, some(ret(min-weight.weight 
                  + energized.get(row).get(col), link(col, min-weight.path))))
        | col == (width - 1) then:
          min-weight = fold({(acc1, x): if x.weight < acc1.weight: x 
              else: acc1 end}, ret(100000000, empty), [list: paths.get-now(row 
                  - 1).get-now(col - 1).value, paths.get-now(row - 1)
                .get-now(col).value])
          paths.get-now(row).set-now(col, some(ret(min-weight.weight 
                  + energized.get(row).get(col), link(col, min-weight.path))))
        | otherwise:
          min-weight = fold({(acc1, x): if x.weight < acc1.weight: x 
              else: acc1 end}, ret(100000000, empty), [list: paths.get-now(row 
                  - 1).get-now(col - 1).value, paths.get-now(row - 1).get-now(
                col).value, paths.get-now(row - 1).get-now(col + 1).value])          
          paths.get-now(row).set-now(col, some(ret(min-weight.weight 
                  + energized.get(row).get(col), link(col, min-weight.path))))
      end
    end
  end

  last-row = paths.get-now(height - 1).to-list-now()
  min-path = fold({(acc1, x): 
      if x.value.weight < acc1.value.weight: 
        x 
      else if within(0.0001)(x.value.weight, acc1.value.weight):
        # if same stream value check start point
        if x.value.path.first < acc1.value.path.first:
          x
        else:
          acc1
        end
      else: 
        acc1 
      end}, last-row.first, last-row).value.path.reverse()
  pixels = input.pixels
  new-pixels = for map2(row from pixels, drop from min-path):
    row.take(drop).append(row.drop(drop + 1))
  end
  image-data-to-image(input.width - 1, input.height, 
    new-pixels)
where:
  seam-DP([image(3,2): color(0,2,0), color(0,0,0),  color(0,3,0), 
      color(1,0,0), color(0,3,0),  color(0,0,4)])
    is [image(2,2): color(0,0,0), color(0,3,0), color(0,3,0), color(0,0,4)]
  seam-DP([image(4, 3): color(100, 46, 1), color(148, 60, 109), color(210, 10, 
        153), color(114, 161, 217), color(115, 68, 96), color(235, 211, 143), 
      color(249, 212, 51), color(172, 116, 209), color(180, 217, 96), color(61,
        243, 184), color(124, 38, 76), color(95, 83, 249)])
    is [image(3,3): color(100, 46, 1), color(148, 60, 109), color(210, 10, 153),
    color(115, 68, 96), color(235, 211, 143), color(172, 116, 209), color(180, 
      217, 96), color(61, 243, 184), color(124, 38, 76)]
  seam-DP([image(2, 2): color(1,1,1), color(1,0,2), color(2,0,1), 
      color(0,3,0)]) is [image(1, 2): color(1, 0, 2), color(0, 3, 0)]
  # all streams same energy
  seam-DP(image2) is [image(1, 2): color(1, 0, 2), color(0, 3, 0)]

  # same energy streams start different place end one place
  seam-DP(image3)
    is [image(4, 3): color(0, 10, 0), color(0, 10, 0), color(0, 0, 0), 
    color(0, 10, 0), color(0, 100, 0), color(0, 10, 0), color(0, 10, 0), 
    color(0, 100, 0), color(0, 200, 0), color(0, 100, 0), color(0, 100, 0), 
    color(0, 200, 0)]

  # same energy streams start same end same diverge middle
  seam-DP(image4)
    is [image(4, 3): color(0, 20, 0), color(0, 0, 0), color(0, 0, 0), color(0, 
      20, 0), color(0, 250, 0), color(0, 100, 0), color(0, 10, 0), color(0, 250,
      0), color(0, 200, 0), color(0, 100, 0), color(0, 100, 0), color(0, 200, 
      0)]
  
  # multiple seams all disjoint
  seam-DP(image5)
    is [image(6, 3): color(0, 0, 0), color(10, 0, 0), color(0, 0, 0), 
    color(10, 0, 0), color(0, 0, 0), color(10, 0, 0), color(0, 0, 0), 
    color(10, 0, 0), color(0, 0, 0), color(10, 0, 0), color(0, 0, 0), 
    color(10, 0, 0), color(0, 0, 0), color(10, 0, 0), color(0, 0, 0), 
    color(10, 0, 0), color(0, 0, 0), color(10, 0, 0)]
end


### MAIN FUNCTIONS

fun liquify-memoization(input :: Image, n :: Number) -> Image:
  doc:```consumes an image and the number of seams to carve out; 
      returns the carved image using memoization```
  ask:
    | n == 0 then: input
    | otherwise:
      energized = energy(input)
      final-row = seam(energized).first
      min-path = fold({(acc1, x): 
          if x.weight < acc1.weight: 
            x 
          else if within(0.0001)(x.weight, acc1.weight):
            # if same stream value check start point
            if x.path.first < acc1.path.first:
              x
            else:
              acc1
            end
          else: 
            acc1 
          end}, final-row.first, final-row).path
      pixels = input.pixels
      new-pixels = for map2(row from pixels, drop from min-path):
        row.take(drop).append(row.drop(drop + 1))
      end
      liquify-memoization(image-data-to-image(input.width - 1, input.height, 
          new-pixels), n - 1)
  end
end

fun liquify-dynamic-programming(input :: Image, n :: Number) -> Image:
  doc:```consumes an image and the number of seams to carve out; 
      returns the carved image using DP```
  ask:
    | n == 0 then: input
    | otherwise: liquify-dynamic-programming(seam-DP(input), n - 1)
  end
end

Their 'common code' library

# Write data bindings here that you'll need for tests in both
# fluid-images-code.arr and fluid-images-tests.arr

### Test Images

image1 = [image(3,2): color(0,2,0), color(0,0,0), color(0,3,0), 
  color(1,0,0), color(0,3,0),  color(0,0,4)]

image2 = [image(2, 2): color(1,1,1), color(1,0,2), color(2,0,1), 
  color(0,3,0)]

image3 = [image(5, 3): color(0,10,0), color(0,0,0), color(0,10,0), 
  color(0,0,0), color(0,10,0), color(0,100,0), color(0,10,0), color(0,100,0)
  , color(0,10,0), color(0,100,0), color(0,200,0), color(0,100,0), 
  color(0,10,0), color(0,100,0), color(0,200,0)]

image4 = [image(5, 3): color(0,20,0), color(0,0,0), color(0,10,0), 
  color(0,0,0), color(0,20,0), color(0,250,0), color(0,10,0), color(0,100,0)
  , color(0,10,0), color(0,250,0), color(0,200,0), color(0,100,0), 
  color(0,250,0), color(0,100,0), color(0,200,0)]

rand-image = [image(4, 3): color(100, 46, 1), color(148, 60, 109), color(210, 
    10, 153), color(114, 161, 217), color(115, 68, 96), color(235, 211, 
    143), color(249, 212, 51), color(172, 116, 209), color(180, 217, 96), 
  color(61, 243, 184), color(124, 38, 76), color(95, 83, 249)]

image5 = [image(7, 3): color(10, 0, 0), color(0,0,0), color(10, 0, 0), 
  color(0,0,0), color(10, 0, 0), color(0,0,0), color(10, 0, 0), color(10, 0, 0)
  , color(0,0,0), color(10, 0, 0), color(0,0,0), color(10, 0, 0), color(0,0,0), 
  color(10, 0, 0), color(10, 0, 0), color(0,0,0), color(10, 0, 0), color(0,0,0),
  color(10, 0, 0), color(0,0,0), color(10, 0, 0)]

image6 = [image(4, 1): color(1,0,0), color(9,0,0), color(3,0,0), color(6,0,0)]

image7 = [image(5, 6): color(0,20,0), color(0,0,0), color(0,100,0), 
    color(0,0,0), color(0,20,0), color(0,250,0), color(100,100,100), 
  color(0,100,25), color(100,100,100), color(0,250,0), color(0,200,0), 
  color(0,100,0), color(0,250,0), color(0,100,0), color(0,200,0), color(0,20,0),
  color(0,100,0), color(0,100,0), color(0,100,0), color(0,20,0), color(0,100,0),
  color(100,100,100), color(100,0,100), color(100,100,100), color(0,100,0), 
  color(0,100,0), color(200,200,200), color(100,0,0), color(200,200,200), 
  color(0,100,0)]

Their tests

# Write your examples and tests in here. These should not be tests of
# implementation-specific details (e.g., helper functions).

### Liquify Memoization

check "liquify-memoization base case":
  # remove no seam
  liquify-memoization(image1, 0) is image1

  # vertical image no removal
  liquify-memoization([image(1,1): color(0,0,0)], 0) is [image(1,1): 
    color(0,0,0)]

  # single row
  liquify-memoization(image6, 1)
    is [image(3, 1): color(1, 0, 0), color(3, 0, 0), color(6, 0, 0)]
end

check "liquify-memoization standard removal":
  # remove one seam
  liquify-memoization(image1, 1) is [image(2,2): color(0,0,0), color(0,3,0), 
    color(0,3,0),  color(0,0,4)]
  
  # remove one from random
  liquify-memoization(rand-image, 1)
    is [image(3, 3): color(100, 46, 1), color(148, 60, 109), color(210, 10, 153)
    , color(115, 68, 96), color(235, 211, 143), color(172, 116, 209), color(180,
      217, 96), color(61, 243, 184), color(124, 38, 76)]
end

check "liquify-memoization multiple seam removal sensitivity":
  # remove two seams
  liquify-memoization(image1, 2) is [image(1,2): color(0,0,0), color(0,3,0)]

  # remove two seems from random image
  liquify-memoization(rand-image, 2) 
    is [image(2, 3): color(148, 60, 109), color(210, 10, 153), color(115, 68, 
      96), color(172, 116, 209), color(61, 243, 184), color(124, 38, 76)]

  # remove left crossing seam then take out another
  liquify-memoization(image7, 2) 
    is [image(3, 6): color(0, 0, 0), color(0, 0, 0), color(0, 20, 0), color(0, 
      250, 0), color(100, 100, 100), color(0, 250, 0), color(0, 200, 0), 
    color(0, 100, 0), color(0, 200, 0),  color(0, 20, 0), color(0, 100, 0), 
    color(0, 20, 0), color(100, 100, 100), color(100, 100, 100), color(0, 100, 
      0), color(100, 0, 0), color(200, 200, 200), color(0, 100, 0)]

  # single row multiple
  liquify-memoization(image6, 2)
    is [image(2, 1): color(3, 0, 0), color(6, 0, 0)]

  # remove all but one
  liquify-memoization(image7, 4)
    is [image(1, 6): color(0, 0, 0), color(0, 250, 0), color(0, 200, 0), 
    color(0, 20, 0), color(0, 100, 0), color(100, 0, 0)]
  
  # remove two from constant
  liquify-memoization(image5, 2)
    is [image(5, 3): color(0, 0, 0), color(0, 0, 0), color(10, 0, 0), color(0,
      0, 0), color(10, 0, 0), color(0, 0, 0), color(0, 0, 0), color(10, 0, 0), 
    color(0, 0, 0), color(10, 0, 0), color(0, 0, 0), color(0, 0, 0), color(10, 
      0, 0), color(0, 0, 0), color(10, 0, 0)]
end

check "liquify-memoization same energy sensitivity":
  # all streams same energy
  liquify-memoization(image2, 1) is [image(1, 2): color(1, 0, 2), 
    color(0, 3, 0)]

  # same energy streams start different place end one place
  liquify-memoization(image3, 1)
    is [image(4, 3): color(0, 10, 0), color(0, 10, 0), color(0, 0, 0), 
    color(0, 10, 0), color(0, 100, 0), color(0, 10, 0), color(0, 10, 0), 
    color(0, 100, 0), color(0, 200, 0), color(0, 100, 0), color(0, 100, 0), 
    color(0, 200, 0)]

  # same energy streams start same end same diverge middle
  liquify-memoization(image4, 1)
    is [image(4, 3): color(0, 20, 0), color(0, 0, 0), color(0, 0, 0), color(0, 
      20, 0), color(0, 250, 0), color(0, 100, 0), color(0, 10, 0), color(0, 250,
      0), color(0, 200, 0), color(0, 100, 0), color(0, 100, 0), color(0, 200, 
      0)]

  # multiple seams all disjoint
  liquify-memoization(image5, 1)
    is [image(6, 3): color(0, 0, 0), color(10, 0, 0), color(0, 0, 0), 
    color(10, 0, 0), color(0, 0, 0), color(10, 0, 0), color(0, 0, 0), 
    color(10, 0, 0), color(0, 0, 0), color(10, 0, 0), color(0, 0, 0), 
    color(10, 0, 0), color(0, 0, 0), color(10, 0, 0), color(0, 0, 0), 
    color(10, 0, 0), color(0, 0, 0), color(10, 0, 0)]
end

### Liquify Dynamic Programming

check "liquify-dynamic-programming base case":
  # remove no seam
  liquify-dynamic-programming(image1, 0) is image1

  # vertical image no removal
  liquify-dynamic-programming([image(1,1): color(0,0,0)], 0) is [image(1,1): 
    color(0,0,0)]

  # single row
  liquify-dynamic-programming(image6, 1)
    is [image(3, 1): color(1, 0, 0), color(3, 0, 0), color(6, 0, 0)]
end

check "liquify-dynamic-programming standard removal":
  # remove one seam
  liquify-dynamic-programming(image1, 1) is [image(2,2): color(0,0,0), 
    color(0,3,0), color(0,3,0),  color(0,0,4)]
  
  # remove one from random
  liquify-dynamic-programming(rand-image, 1)
    is [image(3, 3): color(100, 46, 1), color(148, 60, 109), color(210, 10, 153)
    , color(115, 68, 96), color(235, 211, 143), color(172, 116, 209), color(180,
      217, 96), color(61, 243, 184), color(124, 38, 76)]
end

check "liquify-dynamic-programming multiple seam removal sensitivity":
  # remove two seams
  liquify-dynamic-programming(image1, 2) is [image(1,2): color(0,0,0), 
    color(0,3,0)]

  # remove two seems from random image
  liquify-dynamic-programming(rand-image, 2) 
    is [image(2, 3): color(148, 60, 109), color(210, 10, 153), color(115, 68, 
      96), color(172, 116, 209), color(61, 243, 184), color(124, 38, 76)]

  # remove left crossing seam then take out another
  liquify-dynamic-programming(image7, 2) 
    is [image(3, 6): color(0, 0, 0), color(0, 0, 0), color(0, 20, 0), color(0, 
      250, 0), color(100, 100, 100), color(0, 250, 0), color(0, 200, 0), 
    color(0, 100, 0), color(0, 200, 0),  color(0, 20, 0), color(0, 100, 0), 
    color(0, 20, 0), color(100, 100, 100), color(100, 100, 100), color(0, 100, 
      0), color(100, 0, 0), color(200, 200, 200), color(0, 100, 0)]

  # single row multiple
  liquify-dynamic-programming(image6, 2)
    is [image(2, 1): color(3, 0, 0), color(6, 0, 0)]

  # remove all but one
  liquify-dynamic-programming(image7, 4)
    is [image(1, 6): color(0, 0, 0), color(0, 250, 0), color(0, 200, 0), 
    color(0, 20, 0), color(0, 100, 0), color(100, 0, 0)] 
  
  # remove two from constant
  liquify-dynamic-programming(image5, 2)
    is [image(5, 3): color(0, 0, 0), color(0, 0, 0), color(10, 0, 0), color(0,
      0, 0), color(10, 0, 0), color(0, 0, 0), color(0, 0, 0), color(10, 0, 0), 
    color(0, 0, 0), color(10, 0, 0), color(0, 0, 0), color(0, 0, 0), color(10, 
      0, 0), color(0, 0, 0), color(10, 0, 0)]
end

check "liquify-dynamic-programming same energy sensitivity":
  # all streams same energy
  liquify-dynamic-programming(image2, 1) is [image(1, 2): color(1, 0, 2), 
    color(0, 3, 0)]

  # same energy streams start different place end one place
  liquify-dynamic-programming(image3, 1)
    is [image(4, 3): color(0, 10, 0), color(0, 10, 0), color(0, 0, 0), 
    color(0, 10, 0), color(0, 100, 0), color(0, 10, 0), color(0, 10, 0), 
    color(0, 100, 0), color(0, 200, 0), color(0, 100, 0), color(0, 100, 0), 
    color(0, 200, 0)]

  # same energy streams start same end same diverge middle
  liquify-dynamic-programming(image4, 1)
    is [image(4, 3): color(0, 20, 0), color(0, 0, 0), color(0, 0, 0), color(0, 
      20, 0), color(0, 250, 0), color(0, 100, 0), color(0, 10, 0), color(0, 250,
      0), color(0, 200, 0), color(0, 100, 0), color(0, 100, 0), color(0, 200, 
      0)]

  # multiple seams all disjoint
  liquify-dynamic-programming(image5, 1)
    is [image(6, 3): color(0, 0, 0), color(10, 0, 0), color(0, 0, 0), 
    color(10, 0, 0), color(0, 0, 0), color(10, 0, 0), color(0, 0, 0), 
    color(10, 0, 0), color(0, 0, 0), color(10, 0, 0), color(0, 0, 0), 
    color(10, 0, 0), color(0, 0, 0), color(10, 0, 0), color(0, 0, 0), 
    color(10, 0, 0), color(0, 0, 0), color(10, 0, 0)]
end

Finished

Remainder of the lectures if you want to watch them, we have no more assignments unless you want to try an older assignment that uses the unix shell. We are as he says at the end of lecture 36 officially developers now.

  • Lecture 32 to 34
    • Preview of his PL course CS1730
    • Parsing, let statements, lazy eval
  • Lecture 35
    • Internet Networks explained
  • Lecture 36
    • Security, how basic programs bring in way too much access and node dependency problems

Option: CS173 Programming Languages

If you liked CS19 you'll like CS173 the sequel taught by the same professor.

  • CS173 Programming Languages 2022
    • Lectures torrent
      • Split into 2 videos you play at the same time, the video labelled 'B' is typically the room and board and 'A' is his laptop. The laptop feed will always be the smaller sized video (no sound).
    • Book is free at plai.org and SMoL tutors
    • Assignments (starter code available in previous semesters)
    • DrRacket which you already downloaded/used in the placement for CS19

I go through this here.

Option: 15-850 Advanced Algorithms

I go through this here.

This course teaches the design methods of algorithms and the fastest current runtime. To me it is more interesting to see how they designed all these instead of just being given a recipe of code to memorize. This is a graduate course so they expect you to figure everything out yourself. They have given us this list of resources to help us learn the material which we'll go into as we try and complete this. You learn this by reading the papers they are extremely thorough going from scratch describing each aspect of every algorithm and how the bound is derived. These papers are way more detailed than any algorithms text. We will try and implement these ourselves using competitive programming resources when possible designing our own algorithms.

It won't make sense at first but after you start reading enough of the papers and seeing how they derive bounds it will.

2023 direct links to lectures:

Lecture 01: Introductions 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/5dd7a3c9-ef73-442e-9d64-af8e016fde50.mp4 
Lecture 02: MSTs & Min Cost Arborescence (MCA) 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/bd5580b5-b3fc-46ea-bd01-af90016e783a.mp4 
Lecture 03: LPs and MCAs 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/5d01f36a-0a38-44d8-90fa-af93016f55da.mp4 
Lecture 04: Shortest Paths 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/014eb7e5-6232-4259-a5e0-af95016d7c9f.mp4 
Lecture 05: Low Stretch Spanning Trees 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/77756715-0b4c-4781-b42c-af97017689f8.mp4 
Lecture 06: Shortest Paths cont. 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/0b2036b9-1256-49a8-a8e1-af9a0176e7bf.mp4 
Lecture 07: Matchings 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/5624f2eb-b44f-46ab-b951-af9c0172e43c.mp4 
Lecture 08: The Polynomial Method 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/528376aa-fd8e-4f2e-baca-af9e016d91ae.mp4 
Lecture 09: Weighted Matchings 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/bb9646fd-0aaf-48a5-8da1-afa1016ed694.mp4 
Lecture 10: Weighted Matchings cont. 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/f41d1fd1-3bd1-435a-b8e4-afa3016e33d3.mp4 
Lecture 11: Concentration of Measure 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/88122358-e5cb-4951-9ce4-afa5016fd37b.mp4 
Lecture 12: Concentrations 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/e9b87046-39a3-4456-8e8b-afa8016db9f8.mp4 
Lecture 13: Dimension Reduction 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/2a9678cb-89b3-40b7-aa12-afaa01709787.mp4 
Lecture 14: Streaming Algorithms 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/f612651a-ebaf-47ab-8943-afac016e530e.mp4 
Lecture 15: Experts Algorithm 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/8dd4c0dd-3a71-4a3c-b0e7-afaf016d5a66.mp4 
Lecture 16: Submodular Functions 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/5cddeccf-3d21-4033-8f8a-afb10176e3ea.mp4 
Lecture 17: Experts cont. 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/901216d6-d004-4c2c-8d26-afb301723dba.mp4 
Lecture 18L Experts for LPs 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/0136339b-d0f6-49a2-9662-afb6016fd460.mp4 
Lecture 19 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/d1f82e72-4d0d-435a-b44a-afb80170f9ab.mp4 
Lecture 20: Gradient Descent 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/733d7f7f-5399-471a-a110-afba017134d9.mp4 
Lecture 21: Ellipsoids, Centroids, etc. 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/5d9edc8a-ebdb-4614-b1c3-afc40160a2c5.mp4 
Lecture 22: Ellipsoids &amp; Poly-Time LPs 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/c2a99b4a-2d07-41c6-a02e-afc60162134f.mp4 
Lecture 23: Interior Point Methods 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/b31db8e1-a889-4970-b5bb-afc8016293c6.mp4 
Lecture 24: Approximation Algorithms 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/7d979db1-eb4f-4abe-a32b-afcb0164b878.mp4 
Lecture 25: Approximations &amp; SDPs 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/97018e86-afa1-4eec-9474-afcd01669016.mp4 
Lecture 26: Online Algorithms 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/04c0a530-cf48-4787-b15d-afcf01612487.mp4 
Lecture 27: The Sum of Squares Paradigm 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/0d4ca6c8-cedf-440d-bdab-afda00ec8cae.mp4 
Lecture 28: Martingales &amp; Concentrations 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/7c6bc42f-8cb2-4aa6-9d7b-afdb016279f2.mp4 
Lecture 29: Expander Graphs 
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/831cce2b-fbe3-41ce-bd55-afe0016352b5.mp4 
Lecture 30: HDXs
https://scs.hosted.panopto.com/Panopto/Podcast/Syndication/5a43f3bf-e9e4-46fd-9833-afe2015bf488.mp4  

Home