#!/usr/bin/env python # coding: utf-8 # # Graphs # In[ ]: import networkx as nx def check(actual, expected): if expected != actual: print(f"Function should return the value {expected}, it is returning the value {actual}") else: print(f"Congratulations, the test case passed!") def draw(graph): g = nx.DiGraph(graph) nx.draw(g, with_labels=True, node_size=1000, node_color='#cfc8f4', arrowsize=40) # ## 1. Adjacency matrix # If the nodes of a graph are the numbers from `0` to `n-1`, we can also represent it as an $n \times n$ matrix called the **adjacency matrix**. # # $$\mathbf{A}=\begin{pmatrix} # a_{00} & a_{01} & \cdots & a_{0(n-1)} \\ # a_{10} & a_{11} & \cdots & a_{1(n-1)} \\ # \vdots & \vdots & \ddots & \vdots \\ # a_{(n-1)0} & a_{(n-1)1} & \cdots & a_{(n-1)(n-1)} \\ # \end{pmatrix}$$ # # Its entries $a_{ij}$ are **1** if there is an edge from $i$ to $j$, and **0** otherwise. # # For example, the following graph `g = [[0,1], [0,2], [1,0], [1,2], [2,0]]` # # ![image-3.png](attachment:image-3.png) # # has the adjacency matrix # # $$\mathbf{A}=\begin{pmatrix} # 0 & 1 & 1 \\ # 1 & 0 & 1 \\ # 1 & 0 & 0 \\ # \end{pmatrix}$$ # ### Exercise 1.1 # # What is the **adjacency matrix** of the following graph? # # List of edges: `[[0,1], [0,2], [0,3], [1,0], [1,2], [1,3], [2,0], [2,1], [2,3], [3,0]]` # # Adjacency dict: `{0: [1,2,3], 1:[0,2,3], 2:[0,1,3], 3:[0]}` # # ![image-2.png](attachment:image-2.png) # **Your answer here** #
# Click here to see the solution # # # $$\mathbf{A}=\begin{pmatrix} # 0 & 1 & 1 & 1 \\ # 1 & 0 & 1 & 1 \\ # 1 & 1 & 0 & 1 \\ # 1 & 0 & 0 & 0 \\ # \end{pmatrix}$$ # #
# ### Matrices in Python # # We can represent a matrix as a list of lists in python. # # For example: # # $$\mathbf{A}=\begin{pmatrix} # 0 & 1 & 1 \\ # 1 & 0 & 1 \\ # 1 & 0 & 0 \\ # \end{pmatrix}$$ # # would be represented as # ``` # [[0, 1, 1], [1, 0, 1], [1, 0, 0]] # ``` # ### Exercise 1.2 # Write a function `zeromat(n)` that returns an $n\times n$ matrix whose entries are all 0. # In[ ]: def zeromat(n): # Your code here! # Tests for your code check(zeromat(2), [[0,0],[0,0]]) mat3 = zeromat(3) check(mat3, [[0,0,0],[0,0,0],[0,0,0]]) # When we change one entry, check that only that entry changes. mat3[0][0] = 1 check(mat3, [[1,0,0],[0,0,0],[0,0,0]]) # ### Exercise 1.3 # # Write a function `number_nodes(lst)` that takes a **list of edges** and returns the number of nodes in the graph. # # Hint: Take the maximum of all the numbers appearing in your list, and add 1. # In[ ]: def number_nodes(lst): # Your code here! # Testing your code check(number_nodes([[0, 1], [1, 2]]), 3) check(number_nodes([[0, 0]]), 1) check(number_nodes([[0, 1], [1, 2], [3, 4]]), 5) # ### Exercise 1.4 # # Write a function `list_to_matrix(lst)` that takes a **list of edges** and turns it into an **adjacency matrix**. # # Hint: You can use your function `zeromat` after computing how many nodes are in the graph. # In[ ]: def list_to_matrix(lst): # Your code here! # Testing your code g1 = [[0,1], [0,2], [1,0], [1,2], [2,0]] check(list_to_matrix(g1), [[0, 1, 1], [1, 0, 1], [1, 0, 0]]) g2 = [[0,1], [0,2], [0,3], [1,0], [1,2], [1,3], [2,0], [2,1], [2,3], [3,0]] check(list_to_matrix(g2), [[0, 1, 1, 1], [1, 0, 1, 1], [1, 1, 0, 1], [1, 0, 0, 0]]) # ## 2. Counting walks # A walk of length `n` is a sequence of `n+1` nodes connected by `n` edges. # # ![image.png](attachment:image.png) # # For example, the sequence $(1, 2, 0, 2)$ is a walk of length 3 in our graph. This walk starts at node 1 and ends at node 2. # # The sequence $(0, 1, 1)$ is **not** a walk because there is no edge from 1 to 1. # ### Exercise 2.1 # # In this graph # # ![image-3.png](attachment:image-3.png) # # How many walks of **length 2** exist: # - a) from 0 to 0? # - b) from 0 to 1? # - c) from 0 to 2? # - d) from 1 to 0? # - e) from 1 to 1? # - f) from 1 to 2? # - g) from 2 to 0? # - h) from 2 to 1? # - i) from 2 to 2? # **Your answer here** #
# Click here to see the solution # # - a) 2 -- (0, 1, 0) and (0, 2, 0) # - b) 0 # - c) 1 -- (0, 1, 2) # - d) 1 # - e) 1 # - f) 1 # - g) 0 # - h) 1 # - i) 1 # #
# ### Exercise 2.2 # # In the same graph # # - a) How many walks of **length 3** exist from 0 to 0? # - b) How many walks of **length 3** exist from 0 to 1? # - c) How many walks of **length 3** exist from 0 to 2? # - d) How many walks of **length 4** exist from 0 to 0? # - e) How many walks of **length 4** exist from 0 to 1? # - f) How many walks of **length 4** exist from 0 to 2? # **Your answer here** #
# Click here to see the solution # # - a) 1 -- (0, 1, 2, 0) # - b) 2 -- (0, 1, 0, 1) and (0, 2, 0, 1) # - c) 2 -- (0, 2, 0, 2) and (0, 1, 0, 2) # - d) 4 -- (0, 1, 0, 1, 0) and (0, 1, 0, 2, 0) and (0, 2, 0, 1, 0) and (0, 2, 0, 2, 0) # - e) 1 -- (0, 1, 2, 0, 1) # - f) 3 -- (0, 1, 2, 0, 2) and (0, 2, 0, 1, 2) and (0, 1, 0, 1, 2) # #
# ## 3. Matrix multiplication # # We will now discover an elegant way to compute the number of such paths. # # If $A$ and $B$ are $n \times n$ matrices, # $$\mathbf{A}=\begin{pmatrix} # a_{00} & a_{01} & \cdots & a_{0(n-1)} \\ # a_{10} & a_{11} & \cdots & a_{1(n-1)} \\ # \vdots & \vdots & \ddots & \vdots \\ # a_{(n-1)0} & a_{(n-1)1} & \cdots & a_{(n-1)(n-1)} \\ # \end{pmatrix},\quad\mathbf{B}=\begin{pmatrix} # b_{00} & b_{01} & \cdots & b_{0(n-1)} \\ # b_{10} & b_{11} & \cdots & b_{1(n-1)} \\ # \vdots & \vdots & \ddots & \vdots \\ # b_{(n-1)0} & b_{(n-1)1} & \cdots & b_{(n-1)(n-1)} \\ # \end{pmatrix}$$ # the **matrix product** $AB$ is the matrix # $$\mathbf{C} = \begin{pmatrix} # c_{00} & c_{01} & \cdots & c_{0(n-1)} \\ # c_{10} & c_{11} & \cdots & c_{1(n-1)} \\ # \vdots & \vdots & \ddots & \vdots \\ # c_{(n-1)0} & c_{(n-1)1} & \cdots & c_{(n-1)(n-1)} \\ # \end{pmatrix}$$ # whose entries are # $$c_{ij} = a_{i0} b_{0j} + a_{i1} b_{1j} + \cdots + a_{i(n-1)} b_{(n-1)j}$$ # **Example** # # $$\mathbf{A}=\begin{pmatrix} # 1 & 2 \\ # 0 & 4 \\ # \end{pmatrix}$$ # # $$\mathbf{B}=\begin{pmatrix} # 5 & 3 \\ # -1 & 0 \\ # \end{pmatrix}$$ # # $$\mathbf{AB}=\begin{pmatrix} # 1 & 2 \\ # 0 & 4 \\ # \end{pmatrix} # \begin{pmatrix} # 5 & 3 \\ # -1 & 0 \\ # \end{pmatrix} # =\begin{pmatrix} # 1\cdot5+2\cdot(-1) & 1\cdot3+2\cdot0 \\ # 0\cdot5+4\cdot(-1) & 0\cdot3+4\cdot0 \\ # \end{pmatrix} # =\begin{pmatrix} # 3 & 3 \\ # -4 & 0 \\ # \end{pmatrix}$$ # ### Exercise 3.1 # # Consider the adjacency matrix of our graph from before: # # $$\mathbf{A}=\begin{pmatrix} # 0 & 1 & 1 \\ # 1 & 0 & 1 \\ # 1 & 0 & 0 \\ # \end{pmatrix}$$ # # Compute $A\cdot A$ # **Your answer here** #
# Click here to see the solution # # $$\mathbf{A}=\begin{pmatrix} # 2 & 0 & 1 \\ # 1 & 1 & 1 \\ # 0 & 1 & 1 \\ # \end{pmatrix}$$ # #
# ### Exercise 3.2 # # Compare the entries of $A\cdot A$ with the number of walks of length 2 you counted in **Exercise 2.1**. What do you notice? # **Your answer here** #
# Click here to see the solution # # Let $S = A \cdot A$. The number of paths of length 2 from $i$ to $j$ equals the entry $s_{ij}$ #
# ### Exercise 3.3 # # Write a function `matmul(A, B)` that takes two matrices and returns their **matrix product**. # # Hint: The function `zeromat` from earlier might be useful. # In[ ]: def matmul(a, b): # Your code here! # Testing your code a = [[1,2],[0,4]] b = [[5,3],[-1,0]] check(matmul(a, b), [[3, 3], [-4, 0]]) a = [[0, 1, 1], [1, 0, 1], [1, 0, 0]] check(matmul(a, a), [[2, 0, 1], [1, 1, 1], [0, 1, 1]]) # ### Exercise 3.4 # # Use your code to compute the number of paths of length 2 for our graph with edge list `[[0,1], [0,2], [1,0], [1,2], [2,0]]`. # # Compare with the results of Exercise 3.2 # In[ ]: # Your code here # ### Exercise 3.5 # # For our graph from earlier with adjacency matrix # # $$\mathbf{A}=\begin{pmatrix} # 0 & 1 & 1 \\ # 1 & 0 & 1 \\ # 1 & 0 & 0 \\ # \end{pmatrix}$$ # # use your code to compute $S = A \cdot A \cdot A$. # In[ ]: # Your code here # Compute the entries of $S$ with the number of walks of length 3 for our graph from Exercise 2.2! What do you notice? # **Your answer here** #
# Click here to see the solution # # The number of paths of length 3 from $i$ to $j$ equals the entry $s_{ij}$. # #
# ### Conclusion # More generally, for a graph with adjacency matrix $A$, the number of paths of length `n` from $i$ to $j$ equals the $(i,j)$ entry of $A^n$. # # ### Exercise 3.6 # # We've seen in the previous exercise that it is a bit awkward to compute powers of a matrix using `matmul`. Write a new function `matpow(A, n)` that that computes $A^n$, the matrix $A$ to the $n$-th power, using `matmul`. # In[ ]: def matpow(A, n): # Your code here # Tests for your code a = [[0, 1, 1], [1, 0, 1], [1, 0, 0]] check(matpow(a, 2), [[2, 0, 1], [1, 1, 1], [0, 1, 1]]) check(matpow(a, 3), [[1, 2, 2], [2, 1, 2], [2, 0, 1]]) check(matpow(a, 4), [[4, 1, 3], [3, 2, 3], [1, 2, 2]]) b = [[1, 2],[0, 1]] check(matpow(b, 5), [[1, 10], [0, 1]]) # ### Exercise 3.7 # # Use your function to compute the number of walks of length 4 for our graph. Compare with the results from Exercise 2.2! # # `g = [[0,1], [0,2], [1,0], [1,2], [2,0]]` # # ![image.png](attachment:image.png) # In[ ]: # Your code here # ### Exercise 3.8 # # For this graph: # # `g = [[0,1], [0,2], [0,3], [1,0], [4,2], [1,4], [2,1], [2,3], [3,0]]` # ![image.png](attachment:image.png) # # How many walks of length 5 are there starting at node 0 and ending at node 2? # In[ ]: # Your code here # **Your answer here** #
# Click here to see the solution # # ``` # g = [[0,1], [0,2], [0,3], [1,0], [4,2], [1,4], [2,1], [2,3], [3,0]] # a = list_to_matrix(g) # matpow(a, 5) # ``` # # This gives # # ``` # [[10, 7, 6, 7, 5], # [4, 7, 6, 7, 2], # [6, 4, 6, 4, 3], # [4, 5, 3, 5, 2], # [4, 3, 0, 3, 2]] # ``` # # So the answer is 6 (0th row, 2nd column). #
# ## 4. Why does this work? # # ### Exercise 4.1 # # Think about why this works. # # Hint: If you know the number of paths of length $n-1$ from $x$ to $j$ for all nodes $j$, how can you find out the number of paths of length $n$ from $x$ to $y$? # # How does that relate to multiplying $A^{n-1}$ with $A$, using the definition of matrix multiplication? # **Your answer here** # ## 5. Challenge # # Consider a graph with $N$ vertices numbered from 1 to $N$ and without edges, $N\geq3$. You need to make it connected (every vertice should be reachable from each other) in a such way that sums of numbers of vertices adjacent to each vertice are equal. # # Below you can see such graph for $N=3$. # # # # Here each vertice has sum of nubers of adjacent vertices equals to 3. # # For example for the graph above ```solve(3)``` *may* return ```{1: [3], 2: [3], 3: [1, 2]}```. # # _Hint: solve the problem for even values of $N$ first_ # In[ ]: def solve(n): # Complete the function. It should return the graph adjacency dict of size n+1, with an empty first list. pass # In[ ]: n = 3 check(solve(n), expected={0: [], 1: [3], 2: [3], 3: [1, 2]}) n = 4 check(solve(n), expected={0: [], 1: [2, 3], 2: [1, 4], 3: [1, 4], 4: [2, 3]}) n = 5 check(solve(n), expected={0: [], 1: [2, 3, 5], 2: [1, 4, 5], 3: [1, 4, 5], 4: [2, 3, 5], 5: [1, 2, 3, 4]}) n = 6 check(solve(n), expected={0: [], 1: [2, 3, 4, 5], 2: [1, 3, 4, 6], 3: [1, 2, 5, 6], 4: [1, 2, 5, 6], 5: [1, 3, 4, 6], 6: [2, 3, 4, 5]})