Commit d0d45491 authored by Francois Mares's avatar Francois Mares
Browse files
parents 339eeee3 bb6eb692
......@@ -11,4 +11,6 @@ graphviz/
*.out
*.snm
*.toc
*.csv
\ No newline at end of file
*.csv
*.json
*.obj
\ No newline at end of file
from GrapheDeComposition import GC,MGC
from Monoide import MonoideAleatoire
from Monoide import Monoide,ElementMonoideGC,MonoideGC
from CategorieProduit import CategorieProduit
from CategorieLibre import CategorieLibre
import random
import copy
import itertools
from collections import defaultdict
from config import *
from typing import *
if PROGRESS_BAR:
from tqdm import tqdm
from graphviz import Digraph
class GrapheCompositionAleatoire(GC):
"""Construit un graphe de composition aléatoire."""
def __init__(self, nb_fleches:int = None, proba_arreter_generation_loi_de_compo:float = 1/100,nom:str = "Catégorie Aléatoire"):
def __init__(self, nb_fleches:Union[int,None] = None, nb_tentatives_complexification_loi_de_compo:int = 100,nom:str = "Catégorie Aléatoire"):
"""`nb_fleches` est le nombre de flèches élémentaires dans la catégorie aléatoire.
`nb_tentatives_complexification_loi_de_compo` détermine à quel point la loi de composition sera complexifiée,
si le nombre est faible on obtient une catégorie où les flèches sont isolées."""
if nb_fleches == None:
nb_fleches = random.randint(1,30)
nb_fleches = random.randint(1,20)
GC.__init__(self,nom=nom)
table = defaultdict(lambda:None)
i,j = random.randint(0,nb_fleches-1),random.randint(0,nb_fleches-1)
cases_modifiees = set()
while random.random() > proba_arreter_generation_loi_de_compo:
cases_modifiees |= {(i,j)}
if table[(i,j)] == None:
table[(i,j)] = random.randint(0,nb_fleches-1)
class LoiDeComposition:
def __init__(self):
self.table = defaultdict(lambda:None)
# une classe par source et target de flèches initiales (soit 2*nb_fleches classes initiales)
# la source d'une flèche est représentée par le numéro de la flèche
# la cible d'une flèche est représentée par le numero de la flèche plus le nombre de flèches
self.classe_equiv_vers_fleche = {i:{i} for i in range(nb_fleches*2)} # {numero_classe: {fleche1,fleche2,...}}
self.fleche_vers_classe_equiv = {i:i for i in range(nb_fleches*2)} # {fleche:numero_classe}
def linker_sources_fleches(self,fleche1,fleche2):
classe1,classe2 = self.fleche_vers_classe_equiv[fleche1],self.fleche_vers_classe_equiv[fleche2]
if classe1 != classe2:
#on fusionne les deux classes dans classe1
for fleche in self.classe_equiv_vers_fleche[classe2]:
self.fleche_vers_classe_equiv[fleche] = classe1
self.classe_equiv_vers_fleche[classe1] |= self.classe_equiv_vers_fleche[classe2]
del self.classe_equiv_vers_fleche[classe2]
def linker_cibles_fleches(self,fleche1,fleche2):
fleche1,fleche2 = fleche1+nb_fleches,fleche2+nb_fleches
classe1,classe2 = self.fleche_vers_classe_equiv[fleche1],self.fleche_vers_classe_equiv[fleche2]
if classe1 != classe2:
#on fusionne les deux classes dans classe1
for fleche in self.classe_equiv_vers_fleche[classe2]:
self.fleche_vers_classe_equiv[fleche] = classe1
self.classe_equiv_vers_fleche[classe1] |= self.classe_equiv_vers_fleche[classe2]
del self.classe_equiv_vers_fleche[classe2]
def linker_cible_source_fleches(self,fleche1,fleche2):
'''link la cible de `fleche1` à la source de `fleche2`'''
fleche1 += nb_fleches
classe1,classe2 = self.fleche_vers_classe_equiv[fleche1],self.fleche_vers_classe_equiv[fleche2]
if classe1 != classe2:
#on fusionne les deux classes dans classe1
for fleche in self.classe_equiv_vers_fleche[classe2]:
self.fleche_vers_classe_equiv[fleche] = classe1
self.classe_equiv_vers_fleche[classe1] |= self.classe_equiv_vers_fleche[classe2]
del self.classe_equiv_vers_fleche[classe2]
def tentative_complexification(loi:LoiDeComposition,i:int,j:int,nb_tentatives:int = 3) -> Union[LoiDeComposition,None]:
"""Tente de complexifier la loi de composition en définissant la composition j o i.
Renvoie une nouvelle loi de composition si succès, None sinon."""
nouvelle_loi = copy.deepcopy(loi)
if nouvelle_loi.table[(i,j)] != None:
return None
nouvelle_loi.table[(i,j)] = random.randint(0,nb_fleches-1)
nouvelle_loi.linker_sources_fleches(i,nouvelle_loi.table[(i,j)])
nouvelle_loi.linker_cibles_fleches(j,nouvelle_loi.table[(i,j)])
nouvelle_loi.linker_cible_source_fleches(i,j)
while True:
for a,b,c in itertools.product(range(nb_fleches),repeat=3):
if table[(table[(a,b)],c)] != table[(a,table[(b,c)])]:
# l'associativité n'est pas vérifiée
for case in cases_modifiees:
table[case] = None
cases_modifiees = set()
i,j = random.randint(0,nb_fleches-1),random.randint(0,nb_fleches-1)
break
if cases_modifiees == set():
# l'associativité n'est pas vérifiée
continue
for a,b in itertools.product(range(nb_fleches),repeat=2):
if table[(a,b)] != None:
for c in range(nb_fleches):
if table[(table[(a,b)],c)] != None and table[(b,c)] == None:
# les compositions ne sont pas respectées
i,j = b,c
break
if table[(table[(a,b)],c)] == None and table[(b,c)] != None:
# les compositions ne sont pas respectées
i,j = table[(a,b)],c
break
else:
continue
break
if nouvelle_loi.table[(a,b)] != None and nouvelle_loi.table[(b,c)] != None:
if nouvelle_loi.table[(nouvelle_loi.table[(a,b)],c)] != nouvelle_loi.table[(a,nouvelle_loi.table[(b,c)])]:
# l'associativité n'est pas vérifiée
if nouvelle_loi.table[(nouvelle_loi.table[(a,b)],c)] == None:
#on peut corriger la table
nouvelle_loi.table[(nouvelle_loi.table[(a,b)],c)] = nouvelle_loi.table[(a,nouvelle_loi.table[(b,c)])]
nouvelle_loi.linker_sources_fleches(nouvelle_loi.table[(a,b)],nouvelle_loi.table[(a,nouvelle_loi.table[(b,c)])])
nouvelle_loi.linker_cibles_fleches(c,nouvelle_loi.table[(a,nouvelle_loi.table[(b,c)])])
nouvelle_loi.linker_cible_source_fleches(nouvelle_loi.table[(a,b)],c)
if nouvelle_loi.table[(a,nouvelle_loi.table[(b,c)])] == None:
#on peut corriger la table
nouvelle_loi.table[(a,nouvelle_loi.table[(b,c)])] = nouvelle_loi.table[(nouvelle_loi.table[(a,b)],c)]
nouvelle_loi.linker_sources_fleches(a,nouvelle_loi.table[(nouvelle_loi.table[(a,b)],c)])
nouvelle_loi.linker_cibles_fleches(nouvelle_loi.table[(b,c)],nouvelle_loi.table[(nouvelle_loi.table[(a,b)],c)])
nouvelle_loi.linker_cible_source_fleches(a,nouvelle_loi.table[(b,c)])
else:
# on peut pas corriger la table
return None
break # on a corrigé la table, il faut recommencer le check de l'associativité
else:
#il n'y a eu aucune erreur de composition on valide les changements
cases_modifiees = set()
i,j = random.randint(0,nb_fleches-1),random.randint(0,nb_fleches-1)
#on a une table de loi de composition aléatoire
graphe_equiv = defaultdict(set)
#la source d'une flèche i est représentée par le nombre 2*i
#la cible d'une flèche i est représentée par le nombre 2*i+1
#on va faire le graphe d'équivalence entre ces nombres
for a,b in itertools.product(range(nb_fleches),repeat=2):
if table[(a,b)] != None:
# b o a = talbe[(a,b)]
graphe_equiv[2*b] |= {2*a+1}
graphe_equiv[2*a+1] |= {2*b}
graphe_equiv[2*a] |= {2*table[(a,b)]}
graphe_equiv[2*table[(a,b)]] |= {2*a}
graphe_equiv[2*b+1] |= {2*table[(a,b)]+1}
graphe_equiv[2*table[(a,b)]+1] |= {2*b+1}
elem_a_mapper = {i for i in range(nb_fleches*2)}
application = dict()
dernier_element = 0
while len(elem_a_mapper) > 0:
elem = elem_a_mapper.pop()
dernier_element += 1
self |= {dernier_element}
elem_visites = set()
file = {elem}
while len(file) > 0:
elem = file.pop()
application[elem] = dernier_element
elem_visites |= {elem}
file |= graphe_equiv[elem]-elem_visites
elem_a_mapper -= elem_visites
# aucune erreur d'associativité
break
# ici l'associativité est vérifiée
# on doit checker si la composition est respectée
probleme_composition = True
while probleme_composition:
probleme_composition = False
for classe_equiv in nouvelle_loi.classe_equiv_vers_fleche:
for fleche1 in nouvelle_loi.classe_equiv_vers_fleche[classe_equiv]:
if fleche1 >= nb_fleches:
for fleche2 in nouvelle_loi.classe_equiv_vers_fleche[classe_equiv]:
if fleche2 < nb_fleches:
# fleche1 et fleche2 sont censées être composables
if nouvelle_loi.table[(fleche1-nb_fleches,fleche2)] == None:
# problème de composition
probleme_composition = True
for k in range(nb_tentatives):
nouvelle_loi2 = tentative_complexification(nouvelle_loi,fleche1-nb_fleches,fleche2,nb_tentatives-1) #on tente de complexifier
if nouvelle_loi2 != None:
nouvelle_loi = nouvelle_loi2
break
else:
# on a pas réussi à complexifier
return None
break
else:
continue
break
else:
continue
break
return nouvelle_loi
loi = LoiDeComposition()
if PROGRESS_BAR:
print("Création d'une catégorie aléatoire à "+str(nb_fleches)+" flèches ("+str(nb_tentatives_complexification_loi_de_compo)+' complexification de la loi de composition)')
iterator = tqdm(range(nb_tentatives_complexification_loi_de_compo))
else:
iterator = range(nb_tentatives_complexification_loi_de_compo)
for tentative in iterator:
i,j = random.randint(0,nb_fleches-1),random.randint(0,nb_fleches-1)
loi2 = tentative_complexification(loi,i,j)
if loi2 != None:
loi = loi2
fleches = {i:MGC(application[2*i],application[2*i+1]) for i in range(nb_fleches)}
#on a une table de loi de composition aléatoire
for classe in loi.classe_equiv_vers_fleche:
self |= {classe}
fleches = {i:MGC(loi.fleche_vers_classe_equiv[i],loi.fleche_vers_classe_equiv[i+nb_fleches]) for i in range(nb_fleches)}
self |= set(fleches.values())
for i,j in itertools.product(range(nb_fleches),repeat=2):
if table[(i,j)] != None:
MGC.identifier_morphismes(fleches[j]@fleches[i],fleches[table[(i,j)]])
if loi.table[(i,j)] != None:
MGC.identifier_morphismes(fleches[j]@fleches[i],fleches[loi.table[(i,j)]])
with open("out.csv","w") as f:
f.write(","+",".join(map(str,range(nb_fleches)))+"\n")
for a in range(nb_fleches):
f.write(str(a))
for b in range(nb_fleches):
f.write(','+str(loi.table[(a,b)]))
f.write('\n')
def test_GrapheCompositionAleatoire():
random.seed(1)
for i in range(20):
GCA = GrapheCompositionAleatoire()
for i in range(2):
GCA = GrapheCompositionAleatoire()#random.randint(1,50),random.randint(10,500))
GCA.transformer_graphviz()
GCA.loi_de_composition_to_csv('lois de composition/loi_de_compo.csv')
return GCA
class PetitMonoideAleatoire(MonoideGC):
"""Monoïde fini dont la table de loi de composition interne est choisie aléatoirement.
Les éléments du monoïde seront des flèches nommées par des nombres allant de 1
jusqu'au nombre d'éléments moins un sauf l'identité qui représente l'élément neutre.
La taille maximale de ce monoïde est 5, sinon le temps de calcul est trop grand.
Pour un monoïde aléatoire plus grand, faire un produit de `PetitMonoideAleatoire`."""
def __init__(self, nb_elements:Union[int,None] = None, nom:str = "Petit monoïde aléatoire"):
"""`nb_elements` est le nombre d'éléments du monoïde, si `nb_elements` = 1, alors il n'y a que l'identité dans le monoïde.
`nb_elements` doit être inférieur à 5, sinon le temps de calcul est trop long.
Pour un monoïde plus grand, utiliser `MonoideAleatoire`."""
if nb_elements == None:
nb_elements = random.randint(1,5)
if nb_elements > 5:
raise Exception("Le nombre d'elements d'un PetitMonoideAleatoire doit etre inferieur ou egal a 5.")
elements = list(range(nb_elements)) #le 0 va représenter l'identité
associativite_verifiee = False
while not associativite_verifiee:
table = dict()
associativite_verifiee = True
for a,b in itertools.product(elements,repeat=2):
if (a,b) not in table:
table[(a,b)] = random.choice(elements) if a*b != 0 else a+b
# si a ou b est 0 (le produit est 0) alors on renvoie l'autre element (a+b vaut l'autre element puisque 0 est neutre par l'addition)
# on force l'associativité si on peut
for c in elements:
if (table[(a,b)],c) in table and (b,c) in table:
if (a,table[(b,c)]) not in table:
table[(a,table[(b,c)])] = table[(table[(a,b)],c)]
elif table[(table[(a,b)],c)] != table[(a,table[(b,c)])]:
associativite_verifiee = False
table[(a,table[(b,c)])] = table[(table[(a,b)],c)]
break
if (c,table[(a,b)]) in table and (c,a) in table:
if (table[(c,a)],b) not in table:
table[(table[(c,a)],b)] = table[(c,table[(a,b)])]
elif table[(table[(c,a)],b)] != table[(c,table[(a,b)])]:
associativite_verifiee = False
break
else:
continue
break
if not associativite_verifiee:
continue
#on a une table aléatoire
#maintenant on vérifie l'associativité
associativite_verifiee = False
for a,b,c in itertools.product(elements,repeat=3):
if table[(table[(a,b)],c)] != table[(a,table[(b,c)])]:
break
else:
associativite_verifiee = True
# on a notre table de composition
MonoideGC.__init__(self,nom)
fleches = {i:ElementMonoideGC(str(i)) for i in range(1,nb_elements)}
fleches[0] = self.identite()
self |= {fleches[i] for i in range(1,nb_elements)}
for a,b in itertools.product(elements,repeat=2):
if fleches[a]@fleches[b] != fleches[table[(a,b)]]:
MGC.identifier_morphismes(fleches[a]@fleches[b],fleches[table[(a,b)]])
def facteurs_premiers(n):
while n > 1:
for i in range(2, int(n**0.5)+1):
if not n % i:
n //= i
yield i
break
else:
yield n
break
class MonoideAleatoire(CategorieProduit,Monoide):
"""Monoïde dont la table de loi de composition interne est tirée aléatoirement.
Si le nombre d'éléments du monoïde est supérieur à 5, on créé un produit de `PetitMonoideAleatoire`.
Si le (grand) nombre d'éléments est premier ou peu décomposable, on se réserve le droit de changer
le nombre d'élément pour qu'il soit factorisable en facteurs inferieurs ou égaux à 5."""
def __new__(cls, nb_elements:Union[int,None] = None, nom:str = "Monoïde aléatoire"):
if PROGRESS_BAR:
print("Création d'un monoïde à "+str(nb_elements)+" éléments")
if nb_elements == None:
nb_elements = random.randint(1,20)
while max(facteurs_premiers(nb_elements)) > 5:
nb_elements -= 1
if PROGRESS_BAR:
print("Simplification du nombre d'éléments à "+str(nb_elements))
petits_monoides = tuple(PetitMonoideAleatoire(x) for x in tqdm(list(facteurs_premiers(nb_elements))))
instance = CategorieProduit.__new__(cls,*petits_monoides)
return instance
def __init__(self, nb_elements:int = random.randint(1,20), nom:str = "Monoïde aléatoire"):
CategorieProduit.__init__(self,*self)
Monoide.__init__(self,nom)
def __ior__(self, objets:set) -> 'MonoideAleatoire':
if objets == {tuple(1 for i in range(len(self)))}:
return CategorieLibre.__ior__(self,{tuple(1 for i in range(len(self)))})
elif objets == {1}:
return CategorieLibre.__ior__(self,{tuple(1 for i in range(len(self)))})
raise Exception("Tentative d'ajout d'objet dans un monoide "+str(objets))
def test_MonoideGC():
random.seed(22453)
mon = PetitMonoideAleatoire(5)
mon.transformer_graphviz()
mon.loi_de_composition_to_csv()
mon = MonoideAleatoire(72)
mon.transformer_graphviz()
mon.loi_de_composition_to_csv(destination="lois de composition/monoide.csv")
if __name__ == '__main__':
test_GrapheCompositionAleatoire()
\ No newline at end of file
test_GrapheCompositionAleatoire()
test_MonoideGC()
\ No newline at end of file
......@@ -122,7 +122,7 @@ class CategorieLibre(Categorie):
if not cycle.is_identite:
yield cycle
def enumerer_cycles(self, objet:Any, limite_profondeur:int = 10) -> Generator[Morphisme,None,None]:
def enumerer_cycles(self, objet:Any, limite_profondeur:int = 4) -> Generator[Morphisme,None,None]:
"""Enumère toutes les compositions de `objet` à `objet`.
Si f et g sont des cycles minimaux, on doit énumérer tous les mots d'alphabet {f,g}.
Pour ça on s'intéresse aux compositions qui se réduisent en composition déjà générées.
......
......@@ -86,122 +86,3 @@ class MonoideGC(GrapheDeComposition,Monoide):
raise Exception("Tentative de suppression d'un element de monoide de type inconnu "+str(elem))
GrapheDeComposition.__isub__(self,{elem})
return self
class PetitMonoideAleatoire(MonoideGC):
"""Monoïde fini dont la table de loi de composition interne est choisie aléatoirement.
Les éléments du monoïde seront des flèches nommées par des nombres allant de 1
jusqu'au nombre d'éléments moins un sauf l'indentité qui représente l'élément neutre.
La taille maximale de ce monoïde est 5, sinon le temps de calcul est trop grand.
Pour un monoïde aléatoire plus grand, utiliser un `MonoideAleatoire` qui sera un produit de `PetitMonoideAleatoire`."""
def __init__(self, nb_elements:int = random.randint(1,5), nom:str = "Petit monoïde aléatoire"):
"""`nb_elements` est le nombre d'éléments du monoïde, si `nb_elements` = 1, alors il n'y a que l'identité dans le monoïde.
`nb_elements` doit être inférieur à 5, sinon le temps de calcul est trop long.
Pour un monoïde plus grand, utiliser `MonoideAleatoire`."""
if DEBUG_MONOIDE_ALEATOIRE:
print("Début de création d'un monoïde à "+str(nb_elements)+" éléments")
if nb_elements > 5:
raise Exception("Le nombre d'elements d'un PetitMonoideAleatoire doit etre inferieur ou egal a 5.")
elements = list(range(nb_elements)) #le 0 va représenter l'identité
associativite_verifiee = False
while not associativite_verifiee:
table = dict()
associativite_verifiee = True
for a,b in itertools.product(elements,repeat=2):
if (a,b) not in table:
table[(a,b)] = random.choice(elements) if a*b != 0 else a+b
# si a ou b est 0 (le produit est 0) alors on renvoie l'autre element (a+b vaut l'autre element puisque 0 est neutre par l'addition)
# on force l'associativité si on peut
for c in elements:
if (table[(a,b)],c) in table and (b,c) in table:
if (a,table[(b,c)]) not in table:
table[(a,table[(b,c)])] = table[(table[(a,b)],c)]
elif table[(table[(a,b)],c)] != table[(a,table[(b,c)])]:
associativite_verifiee = False
table[(a,table[(b,c)])] = table[(table[(a,b)],c)]
break
if (c,table[(a,b)]) in table and (c,a) in table:
if (table[(c,a)],b) not in table:
table[(table[(c,a)],b)] = table[(c,table[(a,b)])]
elif table[(table[(c,a)],b)] != table[(c,table[(a,b)])]:
associativite_verifiee = False
break
else:
continue
break
if not associativite_verifiee:
continue
#on a une table aléatoire
#maintenant on vérifie l'associativité
associativite_verifiee = False
for a,b,c in itertools.product(elements,repeat=3):
if table[(table[(a,b)],c)] != table[(a,table[(b,c)])]:
break
else:
associativite_verifiee = True
# on a notre table de composition
MonoideGC.__init__(self,nom)
fleches = {i:ElementMonoideGC(str(i)) for i in range(1,nb_elements)}
fleches[0] = self.identite()
self |= {fleches[i] for i in range(1,nb_elements)}
for a,b in itertools.product(elements,repeat=2):
if fleches[a]@fleches[b] != fleches[table[(a,b)]]:
MGC.identifier_morphismes(fleches[a]@fleches[b],fleches[table[(a,b)]])
if DEBUG_MONOIDE_ALEATOIRE:
print("Fin de création d'un monoïde à "+str(nb_elements)+" éléments")
def facteurs_premiers(n):
while n > 1:
for i in range(2, int(n**0.5)+1):
if not n % i:
n //= i
yield i
break
else:
yield n
break
class MonoideAleatoire(CategorieProduit,Monoide):
"""Monoïde dont la table de loi de composition interne est tirée aléatoirement.
Si le nombre d'éléments du monoïde est supérieur à 5, on créé un produit de `PetitMonoideAleatoire`.
Si le (grand) nombre d'éléments est premier ou peu décomposable, on se réserve le droit de changer
le nombre d'élément pour qu'il soit factorisable en facteurs inferieurs ou égaux à 5."""
def __new__(cls, nb_elements:int = random.randint(1,20), nom:str = "Monoïde aléatoire"):
if DEBUG_MONOIDE_ALEATOIRE:
print("Début de création d'un monoïde à "+str(nb_elements)+" éléments")
while max(facteurs_premiers(nb_elements)) > 5:
nb_elements -= 1
if DEBUG_MONOIDE_ALEATOIRE:
print("Simplification du nombre d'éléments à "+str(nb_elements))
petits_monoides = tuple(PetitMonoideAleatoire(x) for x in facteurs_premiers(nb_elements))
instance = CategorieProduit.__new__(cls,*petits_monoides)
if DEBUG_MONOIDE_ALEATOIRE:
print("Fin de création d'un monoïde à "+str(nb_elements)+" éléments")
return instance
def __init__(self, nb_elements:int = random.randint(1,20), nom:str = "Monoïde aléatoire"):
CategorieProduit.__init__(self,*self)
Monoide.__init__(self,nom)
def __ior__(self, objets:set) -> 'MonoideAleatoire':
if objets == {tuple(1 for i in range(len(self)))}:
return CategorieLibre.__ior__(self,{tuple(1 for i in range(len(self)))})
elif objets == {1}:
return CategorieLibre.__ior__(self,{tuple(1 for i in range(len(self)))})
raise Exception("Tentative d'ajout d'objet dans un monoide "+str(objets))
def test_MonoideGC():
random.seed(22453)
mon = PetitMonoideAleatoire(5)
mon.transformer_graphviz()
mon.loi_de_composition_to_csv()
mon = MonoideAleatoire(7)
mon.transformer_graphviz()
mon.loi_de_composition_to_csv(destination="lois de composition/monoide.csv")
if __name__ == '__main__':
test_MonoideGC()
\ No newline at end of file
digraph categorie {
0
0 -> 0
0 -> 1
0 -> 2
1
1 -> 0
1 -> 1
1 -> 2
2
2 -> 0
2 -> 1
2 -> -2
3
3 -> 3
3 -> 4
4
4 -> 3
4 -> -4
4 -> 7
5
5 -> -3
5 -> -1
5 -> 6
6
6 -> 9
6 -> -6
6 -> -4
6 -> 5
7
7 -> 8
7 -> -5
7 -> 4
7 -> -3
8
8 -> -6
8 -> 11
8 -> 7
9
9 -> -7
9 -> -5
9 -> 6
10
10 -> -8
10 -> 13
10 -> -10
11
11 -> 8
11 -> 12
11 -> -9
12
12 -> 11
12 -> -10
13
13 -> 10
13 -> -9
14
14 -> 17
14 -> -14
15
15 -> 16
15 -> -13
16
16 -> -14
16 -> 15
17
17 -> -13
17 -> 14
18
18 -> 21
19
19 -> -17
20
20 -> -18
20 -> 23
21
21 -> 18
21 -> -19
22
22 -> -22
22 -> -20
23
23 -> 24
23 -> 20