15110 SUMMER SESSION ONE - 2013

Programming Assignment 6 - due Friday, June 7 at 10:30AM using ELECTRONIC HANDIN

Overview

For this assignment, you will create a Ruby source file containing a ruby function(s) implementing each of the problems described below. If you find it useful for any of the problems in this assignment, you may define additional helper functions that your primary function for that problem calls to solve a sub-problem. Definitions for these helper functions should be included in the same Ruby source file as the primary function they help. You should store a source file for each problem in a folder named pa6.

Note: You are responsible for protecting your solutions to these problems from being seen by other students either physically (e.g., by looking over your shoulder) or electronically. (More information can be found in the instructions for Programming Assignment 2.)

Problems

  1. [3 points] Recall that, one way to represent the nodes of a complete binary tree is with an array, where the first element contains the root, the next two elements contain the next level of the tree (the children of the root), the next four elements contain the next level of the tree (two containing the children of the root's left child and then two containing the children of the root's right child), the next eight elements contain the next level contains (two children of each of the nodes at the previous level), and so on, depending on how many levels the tree has.

    The binary tree at left with nodes labeled "a" through "o" would be represented by the Ruby array:

    
       ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o"] 
         0   1   2   3   4   5   6   7   8   9  10  11  12  13  14
       

    This representation can be extended to incomplete trees by using nil to represent the "missing" nodes that would need to be added to make a complete tree. This is the case for the tree to the left, which can be represented by the Ruby array:

       ["a","b","c","d","e",nil,"g","h","i",nil,"k",nil,nil,"n","o"]
       

    Observe that:

    Do you see a pattern? There are simple formulas that can be used to calculate the indices of a node's left child, right child, and parent from that node's index.

    1. Define a function left_index(index) (in left_index.rb) that, when passed the index of a node, will return the index of that node's left child. (You do not need to worry about whether the node has a left child; when the node does not actually have a left child, the function should still return where its left child would be if it had one. You may assume that index is a non-negative integer.)

    2. Define a function right_index(index) (in right_index.rb) that, when passed the index of a node, will return the index of that node's right child. (You do not need to worry about whether the node has a right child; when the node does not actually have a right child, the function should still return where its right child would be if it had one. You may assume that index is a non-negative integer.)

    3. Define a function parent_index(index) (in parent_index.rb) that, when passed the index of a node, will return the index of that node's parent. (You may assume that the index is a positive integer.)

  2. [2 points] The ancestors of a node in a tree are defined to include the node itself, the parent of that node, the parent of the parent, the parent of the parent of the parent, etc., up to and including the root of the node.

    The following algorithm prints out the labels of the ancestors of the node at index i in the array tree, which is an array representation of a complete binary tree as described above.

    1. Print out the label of the node at index i in tree with a new line.
    2. If i is greater than 0, then do the following:
      1. Set j equal to the index of the parent of the node at index i in tree.
      2. Recursively, print out the ancestors of the node at index j of tree.
    3. Return nil.

    In ancestors.rb, using the algorithm above define a function ancestors(tree,i), which requires an array tree encoding a binary tree as described above and an index i of a node in the binary tree. When called, ancestors(tree,i) should print out the labels of the ancestors of the node at index i starting with the node at i and going through the root of the tree.

    You should call your parent_index function from problem 1 to compute the value for j in the algorithm above. You may assume that tree is complete, i is a non-negative index less than the length of tree, and that tree[i] is not nil.

    Example:

    >> load "parent_index.rb"
    => true
    >> load "ancestors.rb"
    => true
    >> tree = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o"]
    => ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o"]
    >> ancestors(tree,8)
    i
    d
    b
    a
    => nil
    >> ancestors(tree,5)
    f
    c
    a
    => nil
    >> ancestors(tree,0)
    a
    => nil
    
  3. [2 points] In a Binary Search Tree, each node has a value that is greater than that of all of the nodes reachable through its left child and that is less than that of all of the nodes reachable through its right child. (We will assume that the tree does not hold two nodes with the same value.) A Binary Search Tree can be stored using an array just as we did in the previous problems.

    The following is an algorithm for searching an array tree encoding a binary search tree to determine whether it contains a node with the value key.

    1. Set curr_index to the index of the root node of the tree.
    2. While curr_index is less than the length of tree and tree[curr_index] is not nil, do the following:
      1. Set curr_value to the value of the node at curr_index in tree.
      2. Return true if curr_value is equal to key.
      3. Set curr_index to the index of the left child of the node at curr_index if key is less than curr_value.
      4. Set curr_index to the index of the right child of the node at curr_index if key is greater than curr_value.
    3. Return false.

    Define a function bst_search?(tree,key) (in bst_search.rb) that uses this algorithm to determine whether the value key occurs in tree, the array representing a binary search tree. Call the functions left_index and right_index that you defined in problem 1 to set curr_index in substeps C and D above..

    Example:

    >> load "left_index.rb"
    => true
    >> load "right_index.rb"
    => true
    >> load "bst_search.rb"
    => true
    >> bstree = [84, 41, 96, 25, 50, nil, 98]
    => [84, 41, 96, 25, 50, nil, 98]
    >> bst_search?(bstree, 50)
    => true
    >> bst_search?(bstree, 51)
    => false
    >> bst_search?(bstree, 41)
    => true
    >> bst_search?(bstree, 90)
    => false
           
  4. Recall that a graph of \(n\) nodes can be represented as an \(n \times n\) matrix where the value at row \(i\) column \(j\) gives the "cost" of going directly from node \(i\) to node \(j\) in the graph.

    The following \(O(n^3)\) algorithm computes the lowest cost (shortest) path from each node in the graph to a given destination node \(d\):

    1. Set \(n\) equal to the number of nodes in the graph.
    2. Set best_cost_to_d equal to a new array with \(n\) elements.
    3. For \(i\) in the range 0 to \(n-1\), do the following:
      1. Set best_cost_to_d[\(i\)] equal to the cost of going directly from node \(i\) to node \(d\).
    4. Repeat the following \(n-2\) times:
      1. For \(i\) in the range 0 to \(n-1\), do the following:
        1. For \(j\) in the range 0 to \(n-1\), do the following:
          1. Let cost_from_i_to_j be the cost of going directly from node \(i\) to node \(j\) in the graph.
          2. Let cost_from_i_to_d_via_j be cost_from_i_to_j + best_cost_to_d[\(j\)].
          3. If cost_from_i_to d_via_j is less than best_cost_to_d[\(i\)] then set best_cost_to_d[\(i\)] to cost_from_i_to_d_via_j.
    5. Return the best_cost_to_d array, which will now contain the minimum cost to to \(d\) from each location.

    Write the following two functions:

    1. [2 points] Write a function shortest_path(graph,d) (in shortest_path.rb) that takes graph (a two-dimensional array holding the costs of traversing the graph's edges as described above) and d (the index of the destination node) and returns an array with the costs of the shortest path to node d from each node in the graph, using the algorithm above as your guide.

      Example usage:

      >> inf = 1.0/0.0
      => Infinity
      >> graph1 = [[0,4,5,6,2],[4,0,10,3,1],[5,10,0,7,9],[6,3,7,0,8],[2,1,9,8,0]]
      => [[0,4,5,6,2],[4,0,10,3,1],[5,10,0,7,9],[6,3,7,0,8],[2,1,9,8,0]]
      >> shortest_path(graph1, 0)
      => [0, 3, 5, 6, 2]
      >> shortest_path(graph1, 3)
      => [6, 3, 7, 0, 4]
      >> graph2 = [[0, 6, 7, 5],[2, 0, 4, inf],[2, inf, 0, 3],[inf, inf, 9, 0]]
      => [[0, 6, 7, 5],[2, 0, 4, inf],[2, inf, 0, 3],[inf, inf, 9, 0]]
      >> shortest_path(graph2, 3)
      => [5, 7, 3, 0]
      
    2. [1 point] Write a function test_sp() (in shortest_path.rb) to test your shortest path algorithm. The function should create a 2D matrix for the graph below and then call your shortest_path function with a destination of node 0, returning its answer.

      Hints:

      • Use 0 to represent the weight of going from a node to itself.

      • You can represent infinite edge weights by using the expression 1.0/0.0 which evaluates to a special floating point number that is treated as being larger than any other floating point number (i.e. infinity).

      Example usage:

      >> test_sp()  
      => [0, 10, 14, 8, 7, 12, 15]
      

    Submission

    You should now have a pa6 directory, containing:

    1. left_index.rb
    2. right_index.rb
    3. parent_index.rb
    4. ancestors.rb
    5. bst_search.rb
    6. shortest_path.rb

    Zip up your directory and upload it using the autolab system.