15110 SUMMER SESSION TWO - 2014

Programming Assignment 6 - due Friday, July 18 at 9:00AM

Overview

For this assignment, you will create a Python source file containing a Python 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 Python 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 a list, 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 Python list:

    
       ["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 None 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 Python list:

       ["a","b","c","d","e",None,"g","h","i",None,"k",None,None,"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.py) 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.py) 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.py) 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 list tree, which is a list 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 not the index of the root, 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 None.

    In ancestors.py, using the algorithm above define a function ancestors(tree,i), which requires a list 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. Note that the algorithm is recursive. Your solution must also be recursive, as specified.

    You should call your parent_index function from problem 1 to compute the value for j in the algorithm above. This means you should have a copy of the parent_index function in your ancestors.py file, at the top of the file before you define the function for ancestors.

    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 None.

    Example:

    > python3 -i ancestors.py
    >>> tree = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o"]
    >>> tree
    ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o"]
    >>> ancestors(tree,8)
    i
    d
    b
    a
    >>> ancestors(tree,5)
    f
    c
    a
    >>> ancestors(tree,0)
    a
    
  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 a list just as we did in the previous problems.

    The following is an algorithm for searching a list called 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 valid and the value of the node at curr_index is not None, 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(bst,key) (in bst_search.py) that uses this algorithm to determine whether the value key occurs in bst, the list 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. This means you should have a copy of the left_index and right_index functions in your bst_search.py file, at the top of the file before you define the function for bst_search.

    Example:

    > python3 -i bst_search.py
    >>> bstree = [84, 41, 96, 25, 50, None, 98]
    >>> bstree
    [84, 41, 96, 25, 50, None, 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 list with \(n\) elements.
    3. For \(i\) in the range 0 to \(n-1\) inclusive, 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\) inclusive, do the following:
        1. For \(j\) in the range 0 to \(n-1\ inclusive), 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 it's cheaper to go to d via node j, so set best_cost_to_d[\(i\)] to cost_from_i_to_d_via_j.
    5. Return the best_cost_to_d list, 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.py) that takes graph (a two-dimensional list holding the costs of traversing the graph's edges as described above) and d (the index of the destination node) and returns an list 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:

      > python3 -i shortest_path.py
      >>> f = float("inf")
      >>> 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]]
      >>> 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]]
      >>> 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, f],[2, f, 0, 3],[f, f, 9, 0]]
      >>> graph2
      [[0, 6, 7, 5],[2, 0, 4, inf],[2, inf, 0, 3],[inf, inf, 9, 0]]
      >>> shortest_path(graph2, 3)
      [5, 7, 3, 0]
      
      In the first example, shortest_path(graph1, 0) give the cost of the shortest path in the first graph from node 0 to all of the other nodes. Note that the cost of the shortest path from node 0 to itself is 0 since you're there already.

      Also note that in the graphs, the cost of going from a node to itself is 0, but the cost from going to a node to another node that is not connected to it is infinity.

    2. [1 point] Write a function test_sp() (also in shortest_path.py) 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.

      Example usage:

      >python3 -i shortest.path.py 
      >>> test_sp()  
      [0, 10, 14, 8, 7, 12, 15]
      

      Looking at the results in this case, it says that the shortest paths to Pittsburgh from all cities are as follows:

      From node:            Cost of shortest path:
      0 (Pittsburgh)        0  (already there)
      1 (Erie)              10 (direct)
      2 (Williamsport)      14 (through State College)
      3 (State College)     8  (direct)
      4 (Harrisburg)        7  (direct)
      5 (Scranton)          12 (through State College)
      6 (Philadelphia)      15 (through State College and Scranton)
      
      Try out other destinations to see what you get.

    Submission

    You should now have a pa6 directory, containing:

    1. left_index.py
    2. right_index.py
    3. parent_index.py
    4. ancestors.py (also containing the function parent_index)
    5. bst_search.py (also containing the functions left_index and right_index)
    6. shortest_path.py (containing the functions shortest_path and test_sp)

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