#!/usr/bin/env python
# coding: utf-8
# ## Week 3 Day 4 Morning - Graphs
# 1) Afternoon lecture - review problems from packet
# 2) Cafeteria office hours 12-2, lab/caf office hours 5-6
# 3) Tomorrow: Test 9-11 , Lecture 11-12 , Lecture 2-3. Boaz public lecture 3:30 (optional)
# In[ ]:
get_ipython().run_line_magic('run', '"boaz_utils.ipynb"')
# # Graphs
# Often in computation we have __data__ from the world, and a __question__ we want to answer about these data.
# To do so, we need to find a __model__ for the data, and a way to translate our question into a __mathematical question about the model__
# 1) Have map of Addis, want to find fastest way to get from national museum to market.
# 2) Facebook is trying to figure out how many friends of friends does the average Ethiopean has.
# 3) Geneticist is trying to find which genes relate to a certain colon cancer.
# ![title](addis_map.jpg)
# What is perhaps most surprising is that these and any many other questions, all use the same mathematical model of a __graph__
# A __graph__ is just a way to store __connections__ between pairs of entities:
# The graph of Addis's roads could be composed of all street intersections, with a connection between intersection $u$ and intersection $v$ if they are directly connected by a road.
# The Facebook graphs is composed of all Facebook users, with a connection between user $u$ and user $v$ if they are friends.
# The gene-symptom interaction graph is composed of all genes and all "symptoms" (also known as phenotypes: some observable differences in people), where gene $u$ is connected to symptom $v$ if there is a correlation between people having the gene $u$ and symptom $v$.
# Mathematically, a graph is a set $V$ of __vertices__ and a set $E$ of pairs of these vertices which is known as the set of __edges__. We say that a vertex $u\in V$ is connected to $v\in V$ if the pair $(u,v)$ is in $E$.
# A graph where $(u,v)\in E$ if and only if $(v,u)\in E$ is known as an __undirected__ graphs. Undirected graphs form an important special case, and we will mostly be interested in those graphs.
# Sometimes the edges (or vertices) of the graph are __labeled__ (often by a number), for example in the case of the road network, we might label every road segment with the average time it takes to travel from one end to the other.
# There are two main representations for graphs. We can always assume the vertices are simply identified by the numbers $1$ to $n$ for some $n$.
#
# The __adjacency list representation__ is an array $L$ where $L[i]$ is the list of all neighbors of the vertex $i$ (i.e., all $j$ such that $(i,j)\in E$)
#
# The __adjacency matrix representation__ is an $n\times n$ two-dimensional array $M$ (i.e., matrix) such that $M[i][j]$ equals $1$ if $j$ is a neighbor of $i$ and equals $0$ otherwise.
# ### Questions
# * If a graph has $n$ vertices and $m$ edges - how big is its adjacency list representation? how big is its adjacency matrix representation?
# * Given a graph $G$ on $n$ vertices and two vertices $i,j$, how long can it take us (in the worst case) to find out if $j$ is a neighbor of $i$ when $G$ is represented in the adjacenecy list form? How long will it take in the adjacenecy matrix form?
# ### Examples:
# In[45]:
G = [[1],[2],[3],[0]]
draw_graph(G)
# In[50]:
G = [[3,1],[0,2],[1,3],[2,0]]
draw_graph(G)
# In[54]:
G = [[1,2,3,4,5,6],[0],[0],[0],[0],[0],[0]]
draw_graph(G)
# In[55]:
n = 20
cycle = [ [(i+1) % n, (i-1) % n] for i in range(n) ]
draw_graph(cycle)
# In[ ]:
def grid_neighbors(i,j,n):
if i==n-1 and j== n-1: return []
if i==n-1:
return [i*n+j+1]
if j==n-1:
return [(i+1)*n+j]
return [n*i+((j+1) % n), n*((i+1) % n)+j]
# In[56]:
n = 5
grid = [ grid_neighbors(i,j,n) for i in range(n) for j in range(n) ]
# In[57]:
draw_graph(grid,'grid_layout')
# # Basic graph functions
# In[58]:
def neighbors(G,u): return G[u]
def isedge(G,u,v):
return v in neighbors(G,u)
def vertices(G):
return list(range(len(G)))
def addedge(G,i,j):
if not j in G[i]: G[i].append(j)
def emptygraph(n):
return [[] for i in range(n)]
# In[61]:
neighbors(cycle,0)
# In[63]:
isedge(cycle,4,7)
# In[64]:
vertices(cycle)
# In[ ]:
def zeros(n): return [0 for i in range(n)]
def printmatrix(M):
for L in M:
print(L)
def list2matrix(G):
n = len(vertices(G))
M = [zeros(n) for i in range(n)]
for i in range(n):
for j in range(n):
if isedge(G,i,j):
M[i][j] = 1
else:
M[i][j] = 0
return M
# __Question:__ Write function `list2matrix(G)` that convertes a graph `G` in adj list format to adj matrix format where `M[i][j]` equals `1`/`0` based on whether `i` is neighbor of `j`
# In[65]:
G = [[3,1],[0,2],[1,3],[2,0]]
M = list2matrix(G)
printmatrix(M)
# In[71]:
def list2matrix(G):
n = len(vertices(G))
M = [[0 for i in range(n)] for j in range(n)]
for i in vertices(G):
for j in vertices(G):
if isedge(G,i,j):
(M[i])[j]=1
return M
G = [[1,4],[0,2],[1,3],[2,4],[3,0]]
printmatrix(list2matrix(G))
# ## Example: Make graph undirected
#
# __Exercise:__ Write a function `undir` that takes a graph `G` and outputs a graph `_G` that such that for every `i,j` the edge `i->j` is in `G` if and only if both `j->i` and `i->j` are in `_G`.
# In[ ]:
def undir(G):
_G = [[] for i in vertices(G)]
for i in vertices(G):
for j in neighbors(G,i):
addedge(_G,i,j)
addedge(_G,j,i)
return _G
# In[78]:
G = [[1],[2],[0]]
undir(G)
# In[79]:
draw_graph(undir(G))