network - shortest path algorithm python
Encontrar un tour euleriano (9)
Estoy tratando de resolver un problema en Udacity descrito de la siguiente manera:
# Find Eulerian Tour
#
# Write a function that takes in a graph
# represented as a list of tuples
# and return a list of nodes that
# you would follow on an Eulerian Tour
#
# For example, if the input graph was
# [(1, 2), (2, 3), (3, 1)]
# A possible Eulerian tour would be [1, 2, 3, 1]
Se me ocurrió la siguiente solución, que aunque no es tan elegante como algunos de los algoritmos recursivos, parece funcionar dentro de mi caso de prueba.
def find_eulerian_tour(graph):
tour = []
start_vertex = graph[0][0]
tour.append(start_vertex)
while len(graph) > 0:
current_vertex = tour[len(tour) - 1]
for edge in graph:
if current_vertex in edge:
if edge[0] == current_vertex:
current_vertex = edge[1]
elif edge[1] == current_vertex:
current_vertex = edge[0]
else:
# Edit to account for case no tour is possible
return False
graph.remove(edge)
tour.append(current_vertex)
break
return tour
graph = [(1, 2), (2, 3), (3, 1)]
print find_eulerian_tour(graph)
>> [1, 2, 3, 1]
Sin embargo, al enviar esto, el alumno me rechaza. Estoy haciendo algo mal? No puedo ver ningún error
Aquí hay un caso que su algoritmo no puede manejar: el gráfico completo en 4 vértices. Al realizar un print tour
, obtienes:
>>> cg4 = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
>>> find_eulerian_tour(cg4)
[0]
[0, 1]
[0, 1, 2]
[0, 1, 2, 0]
[0, 1, 2, 0, 3]
[0, 1, 2, 0, 3, 1]
[0, 1, 2, 0, 3, 1]
[0, 1, 2, 0, 3, 1]
[etc.]
Lo dejaré para que encuentre el problema con su enfoque; podría buscar fácilmente en Google una implementación completa, ya que como no lo hizo, supongo que desea divertirse averiguándolo por usted mismo. : ^)
Editar:
Hmmph. Admito que pensé que era simplemente un error fallido al principio. De todos modos, @WolframH me gano un ejemplo actualizado, pero también puedes ver el gráfico completo en 5 vértices, donde tu código da
[0, 1, 2, 0, 3, 1, 4, 0]
y pierde los bordes (2,3), (2,4) y (3,4).
La pregunta se puede resolver fácilmente que las soluciones anteriores que usan recursión simple.
def find_eulerian_tour(graph):
tour=[]
find_tour(graph[0][0],graph,tour)
return tour
def find_tour(u,E,tour):
for (a,b) in E:
if a==u:
E.remove((a,b))
find_tour(b,E,tour)
elif b==u:
E.remove((a,b))
find_tour(a,E,tour)
tour.insert(0,u)
Este código funciona para cualquier lista de entrada de tuplas y devuelve una lista de la gira. Pls envía sugerencias y cambios si los hay. Gracias @WolframH: Tu código no funciona si existe un bucle en el gráfico y las tuplas se ingresan solo para que falle tu código.
Estaba pasando por el mismo curso en Udacity. E implementé el algoritmo de Hierholzer después de leerlo de Wikipedia. Aquí está el enlace al algoritmo https://en.wikipedia.org/wiki/Eulerian_path
Y abajo está mi código. Sin duda, fue aceptado por el clasificador (después de hacer algunos cambios de Python3 a Python2). :)
#!/usr/bin/env python3
# Find Eulerian Tour
#
# Write a program that takes in a graph
# represented as a list of tuples
# and return a list of nodes that
# you would follow on an Eulerian Tour
#
# For example, if the input graph was
# [(1, 2), (2, 3), (3, 1)]
# A possible Eulerian tour would be [1, 2, 3, 1]
def get_a_tour():
''''''This function returns a possible tour in the current graph and removes the edges included in that tour, from the graph.''''''
global graph
nodes_degree = {} # Creating a {node: degree} dictionary for current graph.
for edge in graph:
a, b = edge[0], edge[1]
nodes_degree[a] = nodes_degree.get(a, 0) + 1
nodes_degree[b] = nodes_degree.get(b, 0) + 1
tour =[] # Finding a tour in the current graph.
loop = enumerate(nodes_degree)
while True:
try:
l = loop.__next__()
index = l[0]
node = l[1]
degree = nodes_degree[node]
try:
if (tour[-1], node) in graph or (node, tour[-1]) in graph:
tour.append(node)
try:
graph.remove((tour[-2], tour[-1]))
nodes_degree[tour[-1]] -= 1 # Updating degree of nodes in the graph, not required but for the sake of completeness.
nodes_degree[tour[-2]] -= 1 # Can also be used to check the correctness of program. In the end all degrees must zero.
except ValueError:
graph.remove((tour[-1], tour[-2]))
nodes_degree[tour[-1]] -= 1
nodes_degree[tour[-2]] -= 1
except IndexError:
tour.append(node)
except StopIteration:
loop = enumerate(nodes_degree)
if len(tour) > 2:
if tour[0] == tour[-1]:
return tour
def get_eulerian_tour():
''''''This function returns a Eulerian Tour for the input graph.''''''
global graph
tour = get_a_tour()
if graph: # If stuck at the beginning, finding additional tour in the graph.
loop = enumerate(tour[: -1])
l = loop.__next__()
i = l[0]
node = l[1]
try:
while True:
if node in list(zip(*graph))[0] or node in list(zip(*graph))[1]:
t = get_a_tour() # Retreivng the additional tour
j = t.index(node)
tour = tour[ : i] + t[j:-1] + t[ :j+1] + tour[i+1: ] # Joining the two tours.
if not graph: # Found Eulerian Tour
return tour # Returning the Eulerian Tour
loop = enumerate(tour[: -1]) # Still stuck? Looping back to search for another tour.
l = loop.__next__()
i = l[0]
node = l[1]
except StopIteration: # Oops! seems like the vertices in the current tour cannot connect to rest of the edges in the graph.
print("Your graph doesn''t seem to be connected")
exit()
else: # Found the Eulerian Tour in the very first call. Lucky Enough!
return tour
# Sample inputs
# graph = [(1, 2), (1, 3), (2, 3), (2, 4), (2, 6), (3, 4), (3, 5), (4, 5), (4, 6)]
# graph = [(1, 2), (1, 3), (2, 3)]
# graph = [(1, 2), (1, 3), (2, 3), (2, 4), (2, 6), (3, 4), (3, 5), (4, 5), (4, 6), (9, 10), (10, 11), (11, 9)]
# graph = [(1, 2), (1, 3), (2, 3), (2, 4), (2, 6), (3, 4), (3, 5), (4, 5), (4, 6), (2, 7), (7, 8), (8, 2)]
# graph = [(1, 2), (1, 3), (2, 3), (2, 4), (2, 6), (3, 4), (3, 5), (4, 5), (4, 6), (1, 5), (5, 6), (1, 6)]
# graph = [(1, 2), (2, 3), (3, 1), (3, 4), (4, 3)]
# graph = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
# graph = [(2, 6), (4, 2), (5, 4), (6, 5), (6, 8), (7, 9), (8, 7), (9, 6)]
# creating a {node: degree} dictionary
nodes_degree = {}
for edge in graph:
a, b = edge[0], edge[1]
nodes_degree[a] = nodes_degree.get(a, 0) + 1
nodes_degree[b] = nodes_degree.get(b, 0) + 1
#checking degree
degrees = nodes_degree.values() # remember it return a view
for degree in degrees:
if degree % 2:
print("Your graph have one or more nodes with odd degrees. Hence an Eulerian Tour is impossible.")
exit()
#finding Eulerian Tour
tour = get_eulerian_tour()
print(tour)
Espero que esto ayude.
Aunque el código falla para gráficos sin dirección pero funciona perfectamente con los dirigidos. Sin embargo, aún así, no resuelve el problema en cuestión desde el lado de Udacity, pero puede tratarse como una versión más baja de la misma. Por favor, no te moleste el Python mal usado ya que todavía soy nuevo en el lenguaje.
Se agregaron dos escenarios de prueba con un nivel bastante bueno de complejidad en la parte inferior.
initialNode = ''''
nglength = 0
in_graphLength = 0
normalizedGraph = list()
path = []
node_dict = {}
mod_flag = ''''
def find_eulerian_tour(graph):
global in_graphLength
in_graphLength = len(graph)
graph = normalize_graph(graph,[],-1,len(graph))
print (path)
return path
def normalize_graph(graph,nG,remNode,length):
counter = 0
global path
global initialNode
global in_graphLength
global nglength
global normalizedGraph
localGraph = list()
path = []
pathList = []
if(remNode != -1):
normalizedGraph = nG
baseNode = 0
if(len(normalizedGraph) != 0):
ini1, ini2 = normalizedGraph[0]
initialNode = ini1
a1,b1 = normalizedGraph[len(normalizedGraph) - 1]
baseNode = b1
if(remNode != -2):
graph.pop(remNode)
if(remNode == -1):
a,b = graph[0]
baseNode = b
normalizedGraph.append(graph[0])
initialNode = a
nglength = 1
graph.pop(0)
i = 0
if(len(graph) != 0):
for n1, n2 in graph:
i = i + 1
if(n1 == baseNode):
localGraph = graph[:]
if(isJunction((n1,n2),localGraph, nglength)):
graph.pop(i-1)
graph.append((n1,n2))
normalize_graph(graph, normalizedGraph, -2,in_graphLength)
break
else:
normalizedGraph.append((n1, n2))
nglength = nglength + 1
normalize_graph(graph, normalizedGraph, i - 1,in_graphLength)
break
else:
if( counter == 0):
counter = counter + 1
a0, b0 = normalizedGraph[0]
for n1, n2 in normalizedGraph:
path.append(n1)
path.append(a0)
path = path
return path
def isJunction((n1,n2), graph, nglength):
global node_dict
count = 0
if(len(graph) > 1):
for a1, a2 in graph:
if (n1 == a1):
count = count + 1
if (count > 1):
if(str(n1) not in node_dict):
key = str(n1)
node_dict[key] = count
else:
return handle_degree(n1)
return modification_needed((n1, n2), graph, nglength)
else:
return False
else:
return False
def handle_degree(n1):
global node_dict
key = str(n1)
if(node_dict.get(key) == 2):
return False
def modification_needed((n1,n2),graph, tmplength):
i = 0
global mod_flag
if( n2 == initialNode):
return True
if(len(graph) > 1):
for b1, b2 in graph:
if(n2 == b1):
i = i + 1
tmplength = tmplength + 1
if (b1,b2) in normalizedGraph:
mod_flag = True
continue
if(tmplength < in_graphLength and b2 == initialNode):
mod_flag = True
continue
else:
graph.pop(i-1)
modification_needed((b1,b2),graph,tmplength)
return mod_flag
#find_eulerian_tour([(1,2),(2,6),(7,2),(6,1),(2,3),(3,5),(3,4),(4,5),(5,7),(7,3),(5,6),(6,7)])
#find_eulerian_tour([(0,4),(1,0),(4,2),(4,8),(2,5),(9,5),(8,9),(5,4),(5,1),(7,1),(3,7),(1,6),(6,3)])
Aquí está el código original en la página web de Gregor Ulm y funciona.
def find_eulerian_tour(graph):
def freqencies():
# save all nodes of edges to my_list
# e.g. [3,4,5,1,2,2,3,5]
my_list = [x for (x, y) in graph]
# get the max num of nodes-->create a list
# set all to 0
# for i in range(5) = 0 1 2 3 4
# so range("5" +1) means
# len=6, result=[0,0,0,0,0,0]
# so that the index = the number itself
result = [0 for i in range(max(my_list) + 1)]
# nodes in my_list, increment
# e.g. [0,1,2,2,1,2]
# 3appears 2times.
for i in my_list:
result[i] += 1
return result
# this is Frequencies of each nodes.
def find_node(tour):
for i in tour:
if freq[i] != 0:
return i
return -1
def helper(tour, next):
find_path(tour, next)
u = find_node(tour)
while sum(freq) != 0:
sub = find_path([], u)
# get the sub_path
# add them together
# when draw to u, turn to sub, and then come back to go on the original tour path
# [:a], start to a; [a+1:] a+1 to end
tour = tour[:tour.index(u)] + sub + tour[tour.index(u) + 1:]
u = find_node(tour)
return tour
def find_path(tour, next):
for (x, y) in graph:
if x == next:
# from "double-graph"
# pop out the current one and its respondent one
# actually it means we delete this edge
current = graph.pop(graph.index((x,y)))
graph.pop(graph.index((current[1], current[0])))
# now add this "next" node into the tour
tour.append(current[0])
# decrement in frequency
freq[current[0]] -= 1
freq[current[1]] -= 1
return find_path(tour, current[1])
# if this "next" node is not connected to any other nodes
# single one
tour.append(next)
return tour
# in graph, all edges get reversed one and be added to graph
# can call it "double-graph"
# it helps to calculate the frequency in find_path
# actually we can regard frequency as degrees for each node
graph += [(y, x) for (x, y) in graph]
freq = freqencies()
# set graph[0][0] as starting point
return helper([], graph[0][0])
graph = [(1, 2), (2, 3), (3, 1)]
print find_eulerian_tour(graph)
Esta solución está optimizada para la complejidad O (V + E) es decir, es lineal en el número de bordes y vértices en el gráfico
Para aquellos que desean ver el código directamente: https://github.com/cubohan/py-algos/blob/master/eulerian_tour.py
Tenga en cuenta que el código infringe la legibilidad y el diseño DRY principalmente, pero después de leer la explicación, puede generar fácilmente su propia versión.
# eulerian_tour.py by cubohan
# circa 2017
#
# Problem statement: Given a list of edges, output a list of vertices followed in an eulerian tour
#
# complexity analysis: O(E + V) LINEAR
def find_eulerian_tour(graph):
edges = graph
graph = {}
degree = {}
start = edges[0][0]
count_e = 0
for e in edges:
if not e[0] in graph:
graph[e[0]] = {}
if not e[0] in degree:
degree[e[0]] = 0
if not e[1] in graph:
graph[e[1]] = {}
if not e[1] in degree:
degree[e[1]] = 0
graph[e[0]][e[1]] = 1
graph[e[1]][e[0]] = 1
degree[e[0]] += 1
degree[e[1]] += 1
count_e += 1
max_d = 0
this_ = 0
for v, d in degree.items():
if not d%2 == 0:
# Eulerian tour not possible as odd degree found!
return False
if d>max_d:
this_ = v
max_d = d
visited_e = {}
def is_visited(i, j):
key = str(sorted([i,j]))
if key in visited_e:
return True
else:
visited_e[key] = True
return False
start = this_
route = [start]
indexof = {}
indexof[start] = 0
while count_e>0:
flag = False
for to_v in graph[this_]:
if not is_visited(to_v, this_):
route.append([to_v])
indexof[to_v] = len(route)-1
degree[to_v] -= 1
if degree[to_v] == 0:
del degree[to_v]
degree[this_] -= 1
if degree[this_] == 0:
del degree[this_]
this_ = to_v
flag = True
count_e -= 1
break
if not flag:
break
for key, v in degree.items():
if v <=0:
continue
try:
ind = indexof[key]
except Exception as e:
continue
this_ = key
while count_e>0:
flag = False
for to_v in graph[this_]:
if not is_visited(to_v, this_):
route[ind].append(to_v)
degree[to_v] -= 1
degree[this_] -= 1
this_ = to_v
flag = True
count_e -= 1
break
if not flag:
break
route_ref = []
for r in route:
if type(r) == list:
for _r in r:
route_ref.append(_r)
else:
route_ref.append(r)
return route_ref
if __name__ == "__main__":
print find_eulerian_tour([(0, 1), (1, 5), (1, 7), (4, 5),(4, 8), (1, 6), (3, 7), (5, 9),(2, 4), (0, 4), (2, 5), (3, 6), (8, 9)])
** Primero, el problema se puede dividir en estas subtareas: **
Construya el gráfico en una estructura más amigable que una lista de bordes para facilitar el procesamiento, es decir, (Listas de adyacencia)
Encuentre los grados de cada vértice para verificar primero si es posible realizar un recorrido euleriano (¿Hay solo grados uniformes? Además, ALMACENE estos valores en un dict con vértice como clave => para usar más adelante)
Construye la gira euleriana
El concepto detrás de mi solución es simple.
Eliges el vértice con el grado más alto como punto de partida y lo configuras como el vértice actual. (Nota: Usted logra esto al mismo tiempo mientras calcula los grados de cada vértice. Almacena todos estos grados en un diccionario).
Inserta el vértice actual en la lista de rutas que es su respuesta (Nota: también haga un diccionario de vértices y sus índices en la lista de rutas. Esto se usará más adelante).
Visita el primer borde del vértice actual si aún no lo ha visitado. (Nota: se mantiene un diccionario de bordes visitados, la clave para este dict es una tupla ordenada del par de vértices que constituyen el borde. Después de visitar un borde, márquelo visitado insertándolo en el dict).
Mantiene un recuento de los grados restantes del vértice actual y el vértice visitado (Esto resultará útil más adelante) (Nota: solo necesita restar 1 del dictado de los grados que genera cada vez que elige un borde)
Cambia el vértice actual al vértice en el otro extremo del borde que decidió visitar.
Repita los pasos 2 a 5 hasta que no pueda encontrar un borde no visitado en el vértice actual. (Nota: esto significa que ha regresado a su vértice inicial)
Ahora considere esto: Observe que los bordes / vértices no visitados constituirán subgrafos en el gráfico principal que tienen las mismas propiedades que el gráfico principal, es decir, que es posible un recorrido euleriano desde cualquiera de los vértices en el subgrafo comenzando y terminando en el mismo vértice.
Todos los bordes no visitados pueden ser visitados tomando recorridos eulerianos en estos subgrafos. Solo necesita fusionar estos sub-tours con la primera gira.
Siguiente:
Recorre todos los vértices del gráfico y construye una suburbanización en el mismo proceso que el listado para el recorrido principal si y solo si el grado reducido de este vértice no es cero
La forma en que estos recorridos se fusionarán con la lista de rutas calculada anteriormente es que reemplaza la posición del vértice que está considerando para comenzar una sutileza en la lista de rutas con la lista de salida de las sub-series y luego aplana esta lista de rutas.
¡Aún no hemos terminado! ¿Qué pasa arriba?
¿Qué sucede cuando obtienes un vértice de grado distinto de cero que NO HA SIDO VISITADO y no está presente en la lista de rutas?
Una advertencia: este es un caso excepcional.
Es posible que encuentre vértices no visitados anteriormente y, por lo tanto, no estarán presentes en la lista de rutas principales.
IGNORAR ESTOS mientras bucle! Uno de los vértices que ya ha visitado con un grado de reducción no nulo está GARANTIZADO para conducir a estos vértices en el subtilo que creará a partir de ellos.
¡¡¿¿Cómo es eso posible??!!
Dibuja un gráfico del caso de prueba dado en el enlace al código y lo entenderás. Haga un seguimiento de lo que está haciendo su algo en cada paso del proceso. ¡Dibujalo! Una imagen es log (N) complejidad para la comprensión y palabras O (n2).
Ah, y tenga en cuenta que esta garantía solo se cumple si todos los bordes en la lista de entrada forman un solo gráfico y no dos gráficos separados inconexos.
También estoy en la misma conferencia, la respuesta de WolframH no funciona para mí. Aquí está mi solución (ha sido aceptada por el clasificador):
Inserta todos los next node
posibles en un montón ( search
) y luego busca en cada uno de ellos mientras grabas.
def next_node(edge, current):
return edge[0] if current == edge[1] else edge[1]
def remove_edge(raw_list, discard):
return [item for item in raw_list if item != discard]
def find_eulerian_tour(graph):
search = [[[], graph[0][0], graph]]
while search:
path, node, unexplore = search.pop()
path += [node]
if not unexplore:
return path
for edge in unexplore:
if node in edge:
search += [[path, next_node(edge, node), remove_edge(unexplore, edge)]]
if __name__ == ''__main__'':
graph = [(1, 2), (2, 3), (3, 1), (3, 4), (4, 3)]
print find_eulerian_tour(graph)
[1, 3, 4, 3, 2, 1]
Aquí hay un caso válido donde falla su algoritmo:
graph = [(1, 2), (2, 3), (3, 1), (3, 4), (4, 3)]
Usa el poder de la print
para descubrir qué le sucede a graph
y current_vertex
.
Otra sugerencia: mueva el else
hacia abajo para que pertenezca a for
y se ejecute cuando el bucle for
no esté roto. Como está ahora, nunca se puede ejecutar. Después de esa corrección, el algoritmo aún falla, por supuesto.
El algoritmo aún falla, por supuesto.
El algoritmo aún falla, por supuesto.
Por favor, no comentes diciendo que el código no funciona. No es así El algoritmo aún falla, incluso si el código siguiente hace lo que el OP tenía en mente. El objetivo era mostrar que el algoritmo del OP es incorrecto, lo que el OP no pudo determinar. Para eso, se necesita una implementación correcta del algoritmo de OP (ver más abajo). Una implementación correcta de un algoritmo incorrecto todavía no es una solución correcta.
Lamento empeorar esta respuesta escribiendo todas estas largas explicaciones, pero la gente sigue quejándose de que el código no funciona (por supuesto, el punto era mostrar que está mal). También rechazaron esta respuesta, probablemente porque esperan poder copiar el código como una solución. Pero este no es el punto, el punto es mostrar al OP que hay un error en su algoritmo.
El código a continuación no encuentra recorridos eulerianos. ¡Busca en otro lado para copiar el código para pasar tus evaluaciones!
def find_eulerian_tour(graph):
tour = []
current_vertex = graph[0][0]
tour.append(current_vertex)
while len(graph) > 0:
print(graph, current_vertex)
for edge in graph:
if current_vertex in edge:
if edge[0] == current_vertex:
current_vertex = edge[1]
else:
current_vertex = edge[0]
graph.remove(edge)
tour.append(current_vertex)
break
else:
# Edit to account for case no tour is possible
return False
return tour
graph = [(1, 2), (2, 3), (3, 1), (3, 4), (4, 3)]
print(find_eulerian_tour(graph))
Salida:
[(1, 2), (2, 3), (3, 1), (3, 4), (4, 3)] 1
[(2, 3), (3, 1), (3, 4), (4, 3)] 2
[(3, 1), (3, 4), (4, 3)] 3
[(3, 4), (4, 3)] 1
False
Puedes imitar el comportamiento del algoritmo BFS y aferrarse a él.
Nota: No he intentado escribir la respuesta usando listas enlazadas porque las listas enlazadas requieren la definición de 2 clases (una para definir los nodos y sus comportamientos, y otra para definir la lista enlazada completa y sus comportamientos). Sin embargo, para una mayor eficiencia en (adjuntar) y (eliminar) comportamientos, debe usar listas vinculadas en lugar de matrices:
def find_eulerian_tour(graph):
nodes = set()
for i in graph:
if not i[0] in nodes:
nodes.add(i[0])
if not i[1] in nodes:
nodes.add(i[1])
tour = []
tempstack = []
graphtemp = []
current_vertex = graph[0][0]
tour.append(current_vertex)
tempstack.append(current_vertex)
last_edge = ()
count = 0
while len(set(tempstack)) + 1 < len(nodes) or (count == 0 or tour[0] != tour[len(tour) - 1]):
count += 1
flag = False
for edge in graph:
if current_vertex in edge and edge != last_edge:
if current_vertex == edge[0]:
current_vertex = edge[1]
else:
current_vertex = edge[0]
last_edge = edge
graphtemp.append(edge)
graph.remove(edge)
tour.append(current_vertex)
tempstack.append(current_vertex)
flag = True
break
if flag == False:
tour.remove(current_vertex)
current_vertex = tempstack[0]
tempstack.remove(tempstack[0])
graph.append(graphtemp[0])
graphtemp.remove(graphtemp[0])
return tour
print find_eulerian_tour([(1,2), (2,3), (3,1)])
print(find_eulerian_tour([(0, 1), (1, 5), (1, 7), (4, 5), (4, 8), (1, 6), (3, 7), (5, 9), (2, 4), (0, 4), (2, 5), (3, 6), (8, 9)]))
print(find_eulerian_tour([(1, 13), (1, 6), (6, 11), (3, 13), (8, 13), (0, 6), (8, 9),(5, 9), (2, 6), (6, 10), (7, 9), (1, 12), (4, 12), (5, 14), (0, 1), (2, 3), (4, 11), (6, 9), (7, 14), (10, 13)]))
print(find_eulerian_tour([(8, 16), (8, 18), (16, 17), (18, 19), (3, 17), (13, 17), (5, 13),(3, 4), (0, 18), (3, 14), (11, 14), (1, 8), (1, 9), (4, 12), (2, 19),(1, 10), (7, 9), (13, 15), (6, 12), (0, 1), (2, 11), (3, 18), (5, 6), (7, 15), (8, 13), (10, 17)]))