Computer Science 60
Principles of Computer Science


Assignment 8: BSTNode and Dynamic Programming    [100 Points]
Due Tuesday, April 8th by 11:59pm

This week's homework is about algorithmic engineering (not a real term, admittedly), but capturing the idea that by creating carefully-designed data structures (binary search trees) and carefully-designed procedures (dynamic programming algorithms), it's possible to gain both aymptotic (big-O) and real (wall-clock) efficiency in solving certain kinds of problems.

But which problems can be solved quickly? That question is a famous open conjecture, known as the P vs. NP conjecture. At the moment, that question is beyond our understanding. But even though we can't fully characterize which problems are amenable to speedups and which ones simply must take a long time, here you'll implement a few of the speedupable ones as a entry point... .

Partners:  This week's problems may be done singly or with pair programming.


Problem 1: a Java binary search tree: BSTNode

30 points

At this point you've written code for Binary Search Trees (BSTs) in two different languages: Racket & Prolog. Here, you'll add our third language by writing a binary-search-tree data structure (class) in Java named BSTNode. For this problem, you should start with the starter BSTNode class and set of JUnit tests in BSTNodeTest.java:

Here are all of this week's files in one (zipped) folder: To use this, you'll need to (1) unzip the folder, (2) create a new Java project, and (3) link the unzipped folder hw8src as a source folder. (Hw7 explains this process with a more complete, step-by-step set of instructions.)

BSTNode has much of its basic functionality already provided. This provides several examples of static vs nonstatic methods -- and lets this problem focus on the most algorithmically interesting facets of binary search trees!

This class is an "open" data structure (a bit unsual for Java) -- meaning that all methods will create new data or reuse old data. Either way, no existing BSTNode objects will be modified or altered. Because no data are ever altered -- new data are created when needed -- structures can be shared without worrying that changes to one will change another. After all, there are no changes!

This is the spirit of data structured in Racket and Prolog - one of the points here is to show that Java can support such constructively-handled structures as well as the more traditional, destructively-handled approach that we used in the List class. Both Racket and Prolog use such constructively-handled data structures.

Methods to write

For this problem you should add these methods to the BSTNode class:

You are welcome to delegate the work of the static methods to existing nonstatic ones, and vice-versa. For insert and delete, however, you'll need to implement at least one of the two! As a guide, here are our earlier Racket implementation of insert and delete:

#lang racket

(define BT1 '( 42
               (20 (7 (1 () ()) (8 () ()))
                   (31 () (41 () ())))
               (100 (60 () ())
                    ()) )  )

(define (make-bst key left right) (list key left right))
(define get-key first)
(define get-left second)
(define get-right third)
(define (leaf? BST) (and (null? (get-left BST)) (null? (get-right BST))))
(define (empty? BST) (null? BST))

;; here is a Racket implementation of insert:
(define (insert new_key oldBST)
  (cond ((empty? oldBST)
         (make-bst new_key '() '()))
        ((equal? (get-key oldBST) new_key)
         oldBST)
        ((< (get-key oldBST) new_key)
         (make-bst
          (get-key oldBST)
          (get-left oldBST)
          (insert new_key (get-right oldBST))))
        (else
         (make-bst
          (get-key oldBST)
          (insert new_key (get-left oldBST))
          (get-right oldBST)))))

(define start (make-bst 42 '() '()))
(define v2 (insert 100 (insert 20 start)))
(define v3 (insert 7 (insert 31 (insert 60 v2))))
(define v4 (insert 1 (insert 8 (insert 41 v3))))

;; a test for several inserts...
(equal? BT1 v4)

;; here is a Racket implementation of find-min.
;; This function presumes that BST is NOT EMPTY!
(define (find-min BST)
  (if (null? (get-left BST))
      (get-key BST)  ;; return the root if the L is empty
      (find-min (get-left BST)))) ;; else descend on the left...


;; here is a Racket implementation of delete:
(define (delete key_to_go oldBST)
  (if (null? oldBST)
      '() ;; return the empty tree
  (let* (
         (root (get-key oldBST))
         (L (get-left oldBST))
         (R (get-right oldBST))
         )
    (if (< key_to_go root)
        (make-bst root (delete key_to_go L) R)
    (if (> key_to_go root)
        (make-bst root L (delete key_to_go R))
    ;; must be the case that key_to_go == root here!
    (if (null? L)
        R ;; whether or not R is empty, it's what we want
    (if (null? R)
        L ;; if R is empty, we want L
        ;; otherwise, we grab the min of the right and make it the root
        (make-bst (find-min R) L (delete (find-min R) R)) )))))))

;; tests for delete...
(define BT1_no42 '( 60
               (20 (7 (1 () ()) (8 () ()))
                   (31 () (41 () ())))
               (100 ()
                    ()) )  )

(define BT1_no100 '( 42
               (20 (7 (1 () ()) (8 () ()))
                   (31 () (41 () ())))
               (60 () ())))

(define BT1_no8 '( 42
               (20 (7 (1 () ()) ())
                   (31 () (41 () ())))
               (100 (60 () ())
                    ()) )  )

(equal? (delete 42 BT1) BT1_no42)
(equal? (delete 100 BT1) BT1_no100)
(equal? (delete 8 BT1) BT1_no8)





Problem 2: Change, Inc.

40 points
(Racket ~ 10; Java ~ 30)

You have been hired by Change, Inc., a pioneer in the business of making change. The firm's motto, "Making positive change throughout the world!" isn't quite true because, strictly speaking, the change is only non-negative and not always strictly positive. The PR folks at Change, Inc. aren't worried about this nuance, however.

Your job is to write software that will make change in any given currency system using the smallest number of coins/bills. The currency system might have very odd denominations - for example, Gringotts, a potential Change, Inc. customer, uses currency whose values are 1, 29, and 493 knuts, respectively - so the software should not impose limits on what denominations it can handle. This software will be embedded in cash registers and used to inform tellers how to make change optimally - or it will simply automate the dispensing of change via robotic change-makers.

You may assume that you have "unlimited" quantities of each coin/bill, and that you may use any combination - including repeats - in making change for a given quantity. Also, you may assume that 1 is always present in the list of denominations -- this ensures that it's always possible to make change for any (nonnegative) target amount!

Your boss, Professor I. Lai, believes that a simple recursive algorithm for this problem will be fast enough for use in the company's cash registers. Although you are skeptical, Prof. Lai is the boss - at least for the moment.

Your first task, then, is to test the viability of a purely recursive approach. To that end, Dr. Lai has requested a Racket function that find the shortest-length list of "coins" that sum to a provided target value.

Racket: min-change and make-ones

In a file named change.rkt file, you should write a function named min-change( target, coin-type-list ) that takes two inputs: first, a target value for which to make change and second, coin-type-list, a list of coins/bill denominations in the currency system listed from smallest to largest. The function should return the shortest-length list of denominations (coins) required to make change for the target value. If there are multiple shortest-length lists, any one of them is OK. You should, however, return the list in ascending-sorted order - feel free to use sort to do this... .

Because this is using recursion, you can assume that no target amount will ever be greater than 42 - the nice thing about this is that you can return a list of 42 1s in order to indicate a "bad" result (one that's no worse than any valid result). This will help for some of the base cases.

Approach: Here, you should employ the use-it-or-lose-it technique!

Here are two sample inputs and outputs - note that the result depends on the "currency system" being used.

Racket> (min-change 42 '(1 5 10 25 50))
(1 1 5 10 25)

Racket> (min-change 42 '(1 5 21 35))
(21 21)

Racket> (min-change 1 '(1 5 21 35))
(1)

Racket> (min-change 0 '(1 5 21 35))
()

Hints:

Submit your code in a file named change.rkt.



Java: the Change class and a dynamic programming solution

After the run-time results emerge from Racket's purely recursive change-computer, Dr. Lai is "reallocated" to a different project - and is replaced by the brilliant Dr. Dee Nomination. Dr. D, as she's called, suggests that dynamic programming is likely to solve the problem much more efficiently. Your next task, therefore, is to re-implement the recursive algorithm that you developed in the previous parts using dynamic programming. This time, you should use Java rather than Racket.

Your file should be called Change.java and your class should be named Change. The Change class should have at least these data members and methods:

Though you're welcome to build it from scratch, if you'd like, a starter file for the Change class is provided. Either way, do use the JUnit tests in ChangeTest.java:

Hints:

Submit your program in Change.java




Part 3: The Floyd-Warshall all-pairs shortest-paths algorithm

20 points

Here are the starter files:

You've been hired away from Change, Inc. to work for Google for the new stealth-mode version of their mapping service, named GoogleSpam. GoogleSpam intends to turn users' mapS experience completely on its head! Specifically, GoogleSpam has collected all of the spam-locations on the planet and will allow its users to estimate the shortest distance among any pair of possible spam-locations (nodes) in their map (graph).

You've been asked to implement the main "smarts" of GoogleSpam, i.e., the Floyd-Warshall all-pairs-shortest-paths algorithm. It takes in a graph as an adjacency matrix, computes the shortest paths between every pair of vertices, and then allows queries - in the form of Java API calls (methods) - for the shortest distance between nodes. (Challenge: to also produce the shortest path between any two nodes - see the extra-credit... .)

The graphs In the FWTest.java file, you will see a number of different maps, named map0, map1, up to map4. Each contains an adjacency matrix that indicates the edge weights from each node to each node. Note that, within the map files, no edge from one vertex to another is represented as -1. This is typically represented as some sort of "infinity" value, and the starter code has a data member INF for that purpose. Be careful, however, Java does not actually compute with -1 as if it were infinity -- you'll need to write conditionals that do the correct thing!

You should create a class named FW (or use the one from the starter file) for your Floyd-Warshall implementation. It should have at least these data members and methods:

In the same spirit as the change problem, you will want a helper function that actually runs the Floyd-Warshall algorithm and builds up the table of shortest-distances!

Overview of the FW algorithm...
We hinted at the FW algorithm in class, but more explanation is certainly warranted! Here are the key points to consider: