Part 4: Exercises

Contents

Part 4: Exercises#

Exercise 50

The variable my_list contains a list of ten integer number from 0 to 9. Study the execution of the following function when it is called as algorithm(my_list, 0):

def algorithm(a_list, pos):
    if pos >= len(a_list):
        return a_list
    else:
        common_division = pos / 2
        floor_division = pos // 2
        if floor_division < common_division:
            a_list.remove(a_list[floor_division])

        return algorithm(a_list, pos + 1)

Exercise 51

The variable my_mat_string contains a string of ten 0-9 digits (e.g. "0000123456"). Study the execution of the following functions when they are called as follows: f(my_mat_string).

def f(mat_string):
    c = 0
    lst = list()

    for chr in mat_string:
        n = int(chr)
        lst.append(n)
        if n / 2 != n // 2:
            c = c + 1

    return g(lst, c)


def g(mat_list, c):
    if c <= 0 or len(mat_list) == 0:
        return 0
    else:
        v = 0
        result = list()

        for i in mat_list:
            if v > 0:
                result.append(i)
            if i > 0 and v == 0:
                v = i

        return v + g(result, c - 1)

Exercise 52

The Euclidean algorithm is a method for computing the greatest common divisor (GCD) of two integers, i.e. the largest number that divides them both without a remainder.

The Euclidean algorithm is based on the principle that the GCD of two numbers does not change if the larger number is replaced by its difference with the smaller number. For example, 3 is the GCD of 15 and 6 (as 15 = 3 × 5 and 6 = 3 × 2), and the same number 3 is also the GCD of 6 and 15 − 6 = 9. Since this replacement reduces the larger of the two numbers, repeating this process gives smaller pairs of numbers successively until the two numbers become equal. When that occurs, that number is the GCD of the original two numbers:

  1. GCD(15, 6) = GCD(15-6=9, 6)

  2. GCD(9, 6) = GCD(9-6=3, 6)

  3. GCD(3, 6) = GCD(3, 6-3)

  4. GCD(3, 3) = 3

Write a recursive algorithm in Python – def euclidean(r, s) – which takes in input two integer numbers s and r greater than zero, and returns their greatest common divisor. Accompany the implementation of the function with the appropriate test cases.

Exercise 53

Write down, using a divide and conquer approach, the body of the following Python function:

def sum(number_list) 

The function takes a list of numbers as input and returns their sum. The use of any form of iteration (e.g. for each and while loops) is prohibited. Accompany the implementation of the function with the appropriate test cases.

Exercise 54

Implement in Python the binary search algorithm – i.e. the recursive function def binary_search(item, ordered_list, start, end).

It takes an item to search (i.e. item), an ordered list and starting and ending positions in the list as input. It returns the position of item in the list, if it is in the list, and None otherwise. The binary search first checks whether the middle item of the list between start and end (inclusive) is equal to item, and returns its position if so. Otherwise, if the middle item is less than item, the algorithm continues the search in the part of the list that follows the middle item. Instead, in case the middle item is greater than item, the algorithm continues the search in the part of the list that precedes the middle item. Accompany the implementation of the function with the appropriate test cases.

As supporting material, Fekete and blinry released a nonverbal definition of the algorithm [Fblinry18a], which is useful to understand the rationale of the binary search steps.

Exercise 55

Implement in Python the partition algorithm – i.e. the non-recursive function def partition(input_list, start, end, pivot_position).

It takes a list and the positions of the first and last elements in the list to consider as inputs. It redistributes all the elements of a list having position included between ​start and ​end on the right of the pivot value input_list[pivot_position] if they are greater than it, and on its left otherwise. Also, the algorithm returns the new position where the pivot value is now stored. For instance, considering ​​my_list = list(["The Graveyard Book", "Coraline", "Neverwhere", "Good Omens", "American Gods"]), the execution of partition(my_list, 1, 4, 1) changes ​my_list as follows: ​list(["The Graveyard Book", "American Gods", "Coraline", "Neverwhere", "Good Omens"]) and 2 will be returned (i.e. the new position of "Coraline"). Note that "The Graveyard Book" has not changed its position in the previous execution since it was not included between the specified start and end positions (i.e. 1 and 4 respectively). Accompany the implementation of the function with the appropriate test cases.

As supporting material, Ang recorded a video which is useful to understand the rationale of the partition steps.

Exercise 56

Implement in Python the divide and conquer quicksort algorithm – i.e. the recursive def quicksort(input_list, start, end)​.

It takes a list and the positions of its first and last elements as inputs. Then, it calls partition(input_list, start, end, start) (defined in the previous exercise) to partition the input list into two slices. Finally, it executes itself recursively on the two partitions (neither of which includes the pivot value since it has already been correctly positioned through the execution of partition). In addition, define the base case of the algorithm appropriately to stop if needed before running the partition algorithm. Accompany the implementation of the function with the appropriate test cases.

As supporting material, Fekete and blinry released a nonverbal definition of the algorithm [Fblinry18b], which is useful to understand the rationale of the quicksort.

Exercise 57

Consider the following function:

def rin(g_name, f_name, idx):
    result = []

    if len(g_name) > 0:
        if g_name[0] in f_name:
            result.append(idx)
        
        idx = idx + 1
        result.extend(rin(g_name[1:len(g_name)], f_name, idx))

    return result 

The variable my_g_name contains the string with your given name in lower case (e.g. "john"), and my_f_name contains the string with your family name in lower case (e.g. "doe"). What is the value returned by calling the function sc as shown as follows:

rin(my_g_name, my_f_name, 0)

Exercise 58

Consider the following function:

def rsel(full_name, mat_string):
    uniq = []
    for c in full_name:
        if c not in uniq:
            uniq.append(c)
    
    r = []
    i = len(mat_string) // 2
    if i > 0:
        n = int(mat_string[i])
        if n < len(uniq):
            r.append(uniq[n])
            new_full_name = full_name[0:n] + full_name[n+1:len(full_name)]
            new_mat_string = mat_string[0:n] + mat_string[n+1:len(mat_string)]
            r.extend(rsel(new_full_name, new_mat_string))
    
    return r

The variable my_mat_string contains the string of all ten numbers of a matriculation number (e.g. "0235145398"), and my_full_name is a string of a full name, all in lowercase with no spaces (e.g. "johndoe"). Write down the value returned by calling the function rsel as shown as follows:

rsel(my_full_name, my_mat_string)

Exercise 59

Consider the following function:

def cnt(mat_string):
    result = 0

    if len(mat_string) > 0:
        n = int(mat_string[0])

        if n % 2 == 0:
            return 1 + cnt(mat_string[1:len(mat_string)])
        else:
            return -1 + cnt(mat_string[1:len(mat_string)])
    
    return result

In the function above, the operation % returns the remainder of the division between two numbers.

The variable my_mat_string contains the string of a 10-digit matriculation number (e.g. "0000123456"). Write down the value returned by calling the function cnt as shown as follows:

cnt(my_mat_string)

Exercise 60

Write an extension of the multiplication function introduced in Chapter “Recursion”, i.e. def multiplication(int_1, int_2, solution_dict), by using a dynamic programming approach. This new function takes as input two integers to multiply and a dictionary with solutions of multiplications between numbers. The function returns the result of the multiplication and, at the same time, modifies the solution dictionary, adding additional solutions when found.

Accompany the implementation of the function with the appropriate test cases.

Exercise 61

Write the body of the Python function def depth_first_visit(node) that takes the root node of a tree as input and returns the list of all its nodes ordered according to a depth-first visit. The depth-first visit proceeds as indicated in Fig. 58, where the numbers indicate the order in which the nodes should be visited.

_images/ex-depth-first-visit.png

Fig. 58 Depth-first visit. Photo by Alexander Drichel, source: Wikipedia.#

Accompany the implementation of the function with the appropriate test cases.

Exercise 62

Write in Python a function def breadth_first_visit(root_node) that does not use any recursion. This function takes a tree’s root node and returns a list of all nodes in breadth-first order. The breadth-first order considers all the nodes of the first level, then those ones of the second level, and so forth. For instance, considering the nodes created in the listing in Chapter “Organising information: trees”, the function called on the node book should return the following list:

[book, chapter_1, chapter_2, text_8, paragraph_1, paragraph_2, paragraph_3, text_7, text_1, quotation_1, text_3, quotation_2, text_5, text_6, text_2, text_4]

Accompany the implementation of the function with the appropriate test cases.

Exercise 63

Write in Python a recursive version of the function defined in the {numref}´part-4-ex-13´.

Accompany the implementation of the function with the appropriate test cases.

Exercise 64

A decision tree is a flowchart-like structure in which each internal node represents a test on an attribute (e.g. whether a coin flip comes up heads or tails), each branch represents the outcome of the test, and each leaf node represents a class label (i.e., a decision taken after computing all attributes). The paths from root to leaf represent classification rules. An example of a decision tree is shown in Fig. 59.

_images/ex-decision.png

Fig. 59 Decision tree.#

The decision tree above allows one to check whether a given number (identified by the variable attribute) is equal to 0. For checking this, supposing to execute such a decision tree passing the number 3 as input, one has (a) to start from the root, (b) to execute the condition (i.e. 3 < 0), (c) to follow the related branch (i.e. false), and (d) to repeat again the process if we arrived in an inner node or (e) to return the result if we arrived in a leaf node.

As shown in the figure, in a decision tree, each internal node always has two children: the left one is reached when the condition the internal node specifies is true, while the right one is reached when the condition the internal node specifies is false.

Write an algorithm in Python – def exec_dt(decision, attribute) – which takes in input a tree decision (which is represented by the root node of the tree defined as an object of the class anytree.Node) describing a decision tree, in which the name of each non-leaf node in the tree is a Python function – it means that we can see the node name as a function that can be executed by passing an input, e.g. considering the node n, we can call the function specified as its name passing the input between parenthesis, as usual: n.name(56). Each Python function, to execute using attribute as input, returns either True or False if the condition defined by that function is satisfied or is not satisfied, respectively. The algorithm returns the leaf node reached by executing the decision tree with the value in attribute.

Accompany the implementation of the function with the appropriate test cases.

Exercise 65

A binary search tree is a binary tree data structure where each node may have at most two children, and the value of each node is greater than (or equal to) all the values in the respective node’s left subtree and less than (or equal to) the ones in its right subtree. It can be built, recursively following an approach which recalls the binary search strategy, starting from a list of ordered items (e.g. a list of integers), where each item becomes a node of a tree, as shown in Fig. 60.

_images/ex-bst.png

Fig. 60 Example of a binary search tree.#

As a reminder, the binary search strategy first checks if the middle item of a list is equal to the item to search for and, in case this is not true, it continues to analyse the part of the list that either precedes or follows the middle item if it is greater than or less than the item to search.

Write a recursive algorithm in Python – def create_bst(ordered_list, parent) – which takes in input an ordered list of integers ordered_list and a parent node parent, and returns the root node of the binary search tree created from the input list. In the first call, the function is run passing None as input for the second parameter parent, e.g.

create_bst([0, 1, 1, 2, 3, 5, 8, 13], None)

and returns the binary search tree (i.e. its root node) shown in the example above.

Accompany the implementation of the function with the appropriate test cases.

Exercise 66

Propose some variation to the implementation of the peg solitaire exercise in Chapter “Backtracking algorithms” to make it more efficient – in particular, avoiding unsuccessful configurations if they have already been encountered previously while looking for a solution.

Accompany the implementation of the function with the appropriate test cases.

Exercise 67

AtariGo is a simplified version of Go. Its rules are pretty simple. Two teams, Black and White, take turns placing a stone (game piece) of their own colour on a vacant point (intersection) of the grid on the board. Once placed, stones do not move. A vacant point adjacent to a stone is called a liberty for that stone. Connected stones formed a group and shared their liberties. A stone or group with no liberties is captured. Black plays first. The first team to capture anything wins.

Suppose you want to develop software that can play AtariGo on a 4x4 board, as shown in Fig. 61, already populated with some stones.

_images/ex-go.png

Fig. 61 An example of a small Go board.#

Suppose we use tuples to define every position in the board, as shown as follows:

(0, 0) (1, 0) (2, 0) (3, 0)
(0, 1) (1, 1) (2, 1) (3, 1)
(0, 2) (1, 2) (2, 2) (3, 2)
(0, 3) (1, 3) (2, 3) (3, 3)

One of the functions to implement returns the set of board positions that are not occupied by any stone and that do not result in the stone being immediately captured if placed. Supposing White has to play on the board in the figure, such a function would return the set containing the tuples (0, 0), (1, 0) and (0, 1) – but not (3, 3) since if White places a stone there it will be immediately captured.

Write a function in Python – def get_good_white_moves(white, black) – that takes in input the set of tuples identifying the stones placed in the 4x4 board in the previous turns by the two players (white and black, respectively) and that returns the set of all the tuples identifying possible places where White can put its stone in the current turn, according to the rules mentioned above. As a simplification, avoid checking the liberties of groups of White stones.

Accompany the implementation of the function with the appropriate test cases.

Exercise 68

We define a labyrinth as a set of tuples representing the cells of its paths. The tuples are organised in an x/y grid, similar to the way used in the listing in Chapter “Backtracking algorithms” for the peg solitaire, such as the one proposed as follows:

      (1,0)       (3,0) (4,0) (5,0)
(0,1) (1,1) (2,1) (3,1)       (5,1)
(0,2)       (2,2)       (4,2) (5,2)
(0,3)       (2,3) (3,3)       (5,3)
(0,4)                   (4,4)      
(0,5) (1,5) (2,5) (3,5) (4,5)      

Write the function solve_labyrinth(paths, entrance, exit, last_move) using a backtracking approach, which takes as input the paths of the labyrinth (such as the ones mentioned above), two tuples representing the entrance and the exit of the labyrinth, and the last move made. The function returns the last move done to reach the exit if the labyrinth has an escape; otherwise, it returns None.

Accompany the implementation of the function with the appropriate test cases.

Exercise 69

A minimax is a recursive algorithm for choosing the next move in a two-player (A and B) game like chess, where all the possible configurations of the board are described as nodes in a tree of moves. A value is associated with each configuration (i.e. each node) and indicates how good it would be for a player (either A or B, depending on the turn) to reach that configuration. If it is A’s turn to move, A gives a value to each of its legal moves, i.e. the child nodes of the one describing the current configuration. The best value for A is the maximum of the values of the children of the configuration in which A has to play, while the best value for B is the minimum of the values of the children of the configuration in which B has to play.

The minimax uses a heuristic function get_value only when a terminal node of the tree of moves is reached or when nodes at the maximum search depth are reached (the maximum depth is specified as input to the algorithm). The other non-leaf nodes inherit their value from a descendant leaf/max-depth node, i.e. either the maximum of the values of the children if A is playing or the minimum of the values of the children if B is playing. An example of minimax execution is shown in Fig. 62.

_images/ex-minimax.png

Fig. 62 Minimax execution.#

Write an algorithm in Python – def minimax(node, max_depth, player_a_moves) – which takes in input a node of the tree of moves, the maximum depth to consider while visiting the tree of moves, and whether the player A is playing (when player_a_moves is True) or its opponent is (when player_a_moves is False), and returns the heuristic value that will be assigned to the input node. The function def get_value(node), for getting the heuristic value associated with a leaf or a node at maximum depth, and another function def get_next_valid_moves(node), used to get all the moves (i.e. children) of a configuration (i.e. a node), are provided (i.e. they must not be developed) and can be directly used in the implementation of the algorithm. Initially, for instance considering the image above, the algorithm will be called as follows:

minimax(root, 2, True)

Accompany the implementation of the function with the appropriate test cases.

Exercise 70

Consider the list of co-authors of Tim Berners-Lee as illustrated in the right box at http://dblp.uni-trier.de/pers/hd/b/Berners=Lee:Tim. Build an undirected graph that contains Tim Berners Lee as the central node, and that links to five nodes representing his top-five co-authors. Also, specify the weight of each edge as an attribute, where the value of the weight is the number of bibliographic resources (articles, proceedings, etc.) Tim Berners-Lee has co-authored with the person linked by that edge.

Exercise 71

Create a directed graph which relates the actors Brad Pitt, Eva Green, George Clooney, Catherine Zeta-Jones, Johnny Depp, and Helena Bonham Carter to the following movies: Ocean’s Twelve, Fight Club, Dark Shadows.

Exercise 72

The Erdős number quantifies the collaborative distance between the mathematician Paul Erdős and another person, measured by the number of scholarly articles they have co-authored. In practice, having the collaboration network of some people described as an undirected graph, where each node represents a person and an edge between two people states that they have coauthored some article together, the goal is to find the minimal distance, computed as the number of edges to traverse, between the node representing Paul Erdős and another input person.

An Erdős number by research group (ENG) is the average number computed by dividing the Erdős numbers of each member of the research group by the number of people in that group.

Write an algorithm in Python – def eng(coauthor_graph, research_group) – which takes in input an undirected graph coauthor_graph (i.e. an object of the class networkx.Graph) describing a collaboration network, in which each node is defined by the string of the name of a person and that includes the node "Paul Erdős", and the list of strings research_group, which contains the strings of the names of people that are member of a research group. The Python function should return the Erdős number for the corresponding research group.

Accompany the implementation of the function with the appropriate test cases.

Exercise 73

A* is a search algorithm for weighted graphs. Starting from a specific start node of a graph, it aims to find a path to the given goal node having the smallest cost (i.e. the least distance travelled, considering the sum of the weights of all the edges crossed from start to goal). Typical implementations of A* use a priority queue to perform the repeated selection of minimum cost nodes to expand, computed using the following formula:

f(n) = g(n) + h(n)

where n is the next node on the path, g(n) is the cost of the path from the start node to n, and h(n) is a heuristic function (h is provided as input of the algorithm) that estimates the cost of the cheapest path from n to the goal.

At the very beginning of the algorithm, the priority queue is initialised with the start node, and f(start) is set to h(start). At each step of the algorithm, the node n with the lowest f(n) value is removed from the priority queue, the f and g values of its neighbours are updated accordingly, and these neighbours are added to the priority queue. The algorithm continues until the node with the lowest f value in the queue is removed (i.e., the goal node), at which point g(goal) is returned.

Write an algorithm in Python – def a_star(graph, start, goal, h) – which takes in input a directed graph (defined according to the networkx library), a start node in the graph, a goal node in the graph, and the function h used to compute f, and returns the cost of the cheapest path from start to goal. All the edges in the graph have specified an attribute weight (an integer) representing the cost to traverse that edge.

Accompany the implementation of the function with the appropriate test cases.

Exercise 74

PageRank is an algorithm used by Google Search to rank web pages in their search engine results. It operates on a directed graph where nodes represent webpages, and each directed edge represents a link from a source webpage to a target webpage. Each node of the graph has an associated PageRank that measures its relative importance within the graph (the greater, the more critical).

In its simplified version, it is computed as follows. It takes as input a directed graph, where each node has a potential PageRank transfer value to share with other nodes, initialised to 1. Then, the algorithm transfers the potential value of a given node to its outbound targets, dividing it equally among all outbound links. For instance, suppose that page B had a link to pages C and A, page C has a link to page A, and page D has links to all three pages. Thus, page B would transfer half of its existing value (0.5) to page A and the other half (0.5) to page C. Page C would transfer all of its existing value (1) to the only page it links to, A. Since D had three outbound links, it would transfer one third of its existing value, or approximately 0.33, to A, B and C. The sum of all the values that are transferred to a given node is the PageRank of that node – for instance, page A will have a PageRank of approximately 1.83.

Write an algorithm in Python – def simplified_pr(g) – which takes in input a directed graph created using the networkx library, and returns a dictionary having as many key-value pairs as the number of nodes in the graph. In particular, each pair maps a node name to its PageRank. It is possible to use the method adj[n] of a graph for getting all the nodes reachable from a node n by following its outbound edges. For instance, in the example above, where the graph is stored as a DiGraph in the variable my_g, my_g.adj["D"] returns a collection containing the nodes A, B, and C.

Accompany the implementation of the function with the appropriate test cases.

Exercise 75

Implement the algorithm introduced in Section “The greedy algorithmic approach” of Chapter “Greedy algorithms” for returning the minimum amount of coins for a change.

Accompany the implementation of the function with the appropriate test cases.

Exercise 76

Suppose one must schedule the maximum number of activities in a day, choosing from a set of available activities, while one can address at most one activity at a time. Each activity is defined by a tuple, where the first element specifies the start time (an integer from 0 to 24, indicating the start hour), and the second element specifies the finish time (an integer from 0 to 24, indicating the finish hour). Develop the Python function def select_activities(set_of_activities) by using a greedy approach. It takes as input a set of day-level activities and returns the list of the maximum number of non-overlapping activities that can be scheduled, ordered by start time. Hint: consider each activity’s finish time and how it may affect the selection.

Accompany the implementation of the function with the appropriate test cases.

Exercise 77

Graph colouring is an assignment of attributes traditionally called “colours” to the vertices of an undirected graph subject to certain constraints, where no two adjacent vertices are of the same colour, as shown in Fig. 63.

_images/ex-colouring.png

Fig. 63 Example of a graph colouring.#

A greedy colouring is a colouring of the vertices of a graph formed by a greedy algorithm that considers the vertices of the graph in sequence and assigns each vertex its first available colour. The algorithm processes the vertices, assigning a colour to each one as it is processed. The colours should be represented by the non-negative integers 0, 1, 2, etc., and each vertex is given the colour with the smallest colour number that is not already used by one of its neighbours.

Write an algorithm in Python – def greedy_colouring(g) – that implements the greedy colouring, which takes in input an undirected graph g, created using the library networkx, and returns a dictionary having the name of the vertex as key and the non-negative integer defining its colour as value. Important: having a graph g, the method g.neighbors(n) returns an iterator (acting as a list, when used in a for each loop) of all the neighbours of the given node n.

Accompany the implementation of the function with the appropriate test cases.

References#

[Fblinry18a]

Sándor P. Fekete and blinry. BINÄRY SEARCH. February 2018. URL: https://idea-instructions.com/binary-search/.

[Fblinry18b]

Sándor P. Fekete and blinry. KVICK SÖRT. March 2018. URL: https://idea-instructions.com/quick-sort/.