Rubis
Contexte
Il existe trois familles de polytopes réguliers s'étendant dans des dimensions infinies:
les simplexes, dont le tétraèdre est un membre (je les désignerai souvent ici sous le nom d'hypertétraèdres, bien que le terme simplex soit plus correct.) Leurs symboles schlafi sont de la forme {3,3,...,3,3}
les n-cubes, dont le cube est membre. Leurs symboles schlafi sont de la forme{4,3,...,3,3}
les orthoplexes, dont l'octaèdre est membre (je les désignerai souvent ici par hyperoctaèdres). Leurs symboles schlafi sont de la forme {3,3,...,3,4}
Il existe une autre famille infinie de polytopes réguliers, symbole {m}
, celle des polygones bidimensionnels, qui peuvent avoir un nombre quelconque d'arêtes m.
En plus de cela, il n'y a que cinq autres cas spéciaux de polytope régulier: l'icosaèdre {3,5}
et le dodécaèdre tridimensionnels {5,3}
; leurs analogues à 4 dimensions les cellules 600 {3,3,5}
et 120 cellules {5,3,3}
; et un autre polytope à 4 dimensions, les 24 cellules {3,4,3}
(dont les analogues les plus proches en 3 dimensions sont le cuboctaèdre et son double le dodécaèdre rhombique.)
Fonction principale
Ci-dessous se trouve la polytope
fonction principale qui interprète le symbole schlafi. Il attend un tableau de nombres et renvoie un tableau contenant un groupe de tableaux comme suit:
Un tableau de tous les sommets, chacun étant exprimé sous la forme d'un tableau de coordonnées à n éléments (où n est le nombre de dimensions)
Un tableau de toutes les arêtes, chacune exprimée en 2 éléments d'indices de sommet
Un tableau de toutes les faces, chacune exprimée comme un m-élément d'indices de sommet (où m est le nombre de sommets par face)
et ainsi de suite selon le nombre de dimensions.
Il calcule lui-même les polytopes 2D, appelle des fonctions pour les 3 familles de dimensions infinies et utilise des tables de recherche pour les cinq cas spéciaux. Il s'attend à trouver les fonctions et les tables déclarées au-dessus.
include Math
#code in subsequent sections of this answer should be inserted here
polytope=->schl{
if schl.size==1 #if a single digit calculate and return a polygon
return [(1..schl[0]).map{|i|[sin(PI*2*i/schl[0]),cos(PI*2*i/schl[0])]},(1..schl[0]).map{|i|[i%schl[0],(i+1)%schl[0]]}]
elsif i=[[3,5],[5,3]].index(schl) #if a 3d special, lookup from tables
return [[vv,ee,ff],[uu,aa,bb]][i]
elsif i=[[3,3,5],[5,3,3],[3,4,3]].index(schl) #if a 4d special. lookup fromm tables
return [[v,e,f,g],[u,x,y,z],[o,p,q,r]][i]
elsif schl.size==schl.count(3) #if all threes, call tetr for a hypertetrahedron
return tetr[schl.size+1]
elsif schl.size-1==schl.count(3) #if all except one number 3
return cube[schl.size+1] if schl[0]==4 #and the 1st digit is 4, call cube for a hypercube
return octa[schl.size+1] if schl[-1]==4 #and the last digit is 4, call octa for a hyperoctahedron
end
return "error" #in any other case return an error
}
Fonctions pour les familles tétraèdre, cube et octaèdre
https://en.wikipedia.org/wiki/Simplex
https://en.wikipedia.org/wiki/5-cell (simplex 4d)
http://mathworld.wolfram.com/Simplex.html
Explication de la famille des tétraèdres - coordonnées
un simplex / hypertétraèdre à n dimensions a n + 1 points. Il est très facile de donner les sommets du simplexe à n dimensions en n + 1 dimensions.
(1,0,0),(0,1,0),(0,0,1)
Décrit ainsi un triangle 2D intégré dans 3 dimensions et (1,0,0,0),(0,1,0,0),(0,0,1,0),(0,0,0,1)
décrit un tétraèdre 3D intégré dans 4 dimensions. Ceci est facilement vérifié en confirmant que toutes les distances entre les sommets sont sqrt (2).
Divers algorithmes compliqués sont fournis sur Internet pour trouver les sommets du simplexe à n dimensions dans l'espace à n dimensions. J'en ai trouvé une remarquablement simple dans les commentaires de Will Jagy sur cette réponse /mathpro//a/38725 . Le dernier point se trouve sur la ligne p=q=...=x=y=z
à une distance de sqrt (2) des autres. Ainsi, le triangle ci-dessus peut être converti en tétraèdre en ajoutant un point à l'un (-1/3,-1/3,-1/3)
ou à l'autre (1,1,1)
. Ces 2 valeurs possibles des coordonnées du dernier point sont données par (1-(1+n)**0.5)/n
et(1+(1+n)**0.5)/n
Comme la question dit que la taille du n-tope n'a pas d'importance, je préfère multiplier par n et utiliser les coordonnées (n,0,0..0)
jusqu'au (0..0,0,n)
point final (t,t,..,t,t)
où t = 1-(1+n)**0.5
pour plus de simplicité.
Comme le centre de ce tétraèdre n'est pas à l'origine, une correction de toutes les coordonnées doit être effectuée par la ligne s.map!{|j|j-((1-(1+n)**0.5)+n)/(1+n)}
qui trouve à quelle distance le centre est de l'origine et le soustrait. J'ai gardé cela comme une opération distincte. Cependant, j'ai utilisé s[i]+=n
où s[i]=n
ferait, pour faire allusion au fait que lorsque le tableau est initialisé par s=[0]*n
nous pourrions mettre le bon décalage ici à la place et faire la correction de centrage au début plutôt qu'à la fin.
Explication de la famille des tétraèdres - topologie des graphes
Le graphe du simplexe est le graphe complet: chaque sommet est connecté exactement une fois à chaque autre sommet. Si nous avons un n simplex, nous pouvons supprimer n'importe quel sommet pour donner un n-1 simplex, jusqu'au point où nous avons un triangle ou même une arête.
Nous avons donc un total de 2 ** (n + 1) éléments à cataloguer, chacun représenté par un nombre binaire. Cela va de tous les 0
s pour le néant, à un 1
pour un sommet et à deux 1
s pour une arête, jusqu'à tous les 1
s pour le polytope complet.
Nous avons mis en place un tableau de tableaux vides pour stocker les éléments de chaque taille. Ensuite, nous bouclons de zéro à (2 ** n + 1) pour générer chacun des sous-ensembles possibles de sommets et les stockons dans le tableau en fonction de la taille de chaque sous-ensemble.
Nous ne sommes pas intéressés par quelque chose de plus petit qu'un bord (un sommet ou un zéro) ni par le polytope complet (car le cube complet n'est pas donné dans l'exemple de la question), nous retournons donc tg[2..n]
pour supprimer ces éléments indésirables. Avant de revenir, nous plaçons [tv] contenant les coordonnées du sommet sur le début.
code
tetr=->n{
#Tetrahedron Family Vertices
tv=(0..n).map{|i|
s=[0]*n
if i==n
s.map!{(1-(1+n)**0.5)}
else
s[i]+=n
end
s.map!{|j|j-((1-(1+n)**0.5)+n)/(1+n)}
s}
#Tetrahedron Family Graph
tg=(0..n+1).map{[]}
(2**(n+1)).times{|i|
s=[]
(n+1).times{|j|s<<j if i>>j&1==1}
tg[s.size]<<s
}
return [tv]+tg[2..n]}
cube=->n{
#Cube Family Vertices
cv=(0..2**n-1).map{|i|s=[];n.times{|j|s<<(i>>j&1)*2-1};s}
#Cube Family Graph
cg=(0..n+1).map{[]}
(3**n).times{|i| #for each point
s=[]
cv.size.times{|j| #and each vertex
t=true #assume vertex goes with point
n.times{|k| #and each pair of opposite sides
t&&= (i/(3**k)%3-1)*cv[j][k]!=-1 #if the vertex has kingsmove distance >1 from point it does not belong
}
s<<j if t #add the vertex if it belongs
}
cg[log2(s.size)+1]<<s if s.size > 0
}
return [cv]+cg[2..n]}
octa=->n{
#Octahedron Family Vertices
ov=(0..n*2-1).map{|i|s=[0]*n;s[i/2]=(-1)**i;s}
#Octahedron Family Graph
og=(0..n).map{[]}
(3**n).times{|i| #for each point
s=[]
ov.size.times{|j| #and each vertex
n.times{|k| #and each pair of opposite sides
s<<j if (i/(3**k)%3-1)*ov[j][k]==1 #if the vertex is located in the side corresponding to the point, add the vertex to the list
}
}
og[s.size]<<s
}
return [ov]+og[2..n]}
explication des familles de cubes et d'octaèdres - coordonnées
Le n-cube a des 2**n
sommets, chacun représenté par un tableau de n 1
s et -1
s (toutes les possibilités sont permises.) Nous itérer à travers des index 0
à 2**n-1
la liste de tous les sommets, et de construire un tableau pour chaque sommet en parcourant les bits du indexer et ajouter -1
ou 1
au tableau (du bit le moins significatif au bit le plus significatif.) Ainsi, le binaire 1101
devient le point 4d [1,-1,1,1]
.
Le n-octaèdre ou n-orthoplexe a des 2n
sommets, avec toutes les coordonnées nulles sauf une, qui peut être 1
ou -1
. L'ordre des sommets dans le tableau généré est [[1,0,0..],[-1,0,0..],[0,1,0..],[0,-1,0..],[0,0,1..],[0,0,-1..]...]
. Notez que comme l'octaèdre est le dual du cube, les sommets de l'octaèdre sont définis par les centres des faces du cube qui l'entoure.
Explication des familles de cubes et d'octaèdres - topologie des graphes
Une certaine inspiration a été tirée des côtés de l' hypercube et du fait que l'hyperoctaèdre est le double de l'hypercube.
Pour le n-cube, il y a des 3**n
éléments à cataloguer. Par exemple, le cube 3 a 3**3
= 27 éléments. Cela peut être vu en étudiant un cube de rubik, qui a 1 centre, 6 faces, 12 arêtes et 8 sommets pour un total de 27. Nous itérons par -1,0 et -1 dans toutes les dimensions définissant un n-cube de longueur latérale 2x2x2 .. et renvoie tous les sommets qui ne sont PAS du côté opposé du cube. Ainsi, le point central du cube renvoie tous les 2 ** n sommets, et l'éloignement d'une unité du centre le long de n'importe quel axe réduit le nombre de sommets de moitié.
Comme pour la famille des tétraèdres, nous commençons par générer un tableau vide de tableaux et le remplissons en fonction du nombre de sommets par élément. Notez que parce que le nombre de sommets varie comme 2 ** n lorsque nous montons à travers des arêtes, des faces, des cubes, etc., nous utilisons log2(s.size)+1
au lieu de simplement s.size
. Encore une fois, nous devons supprimer l'hypercube lui-même et tous les éléments avec moins de 2 sommets avant de revenir de la fonction.
La famille des octaèdres / orthoplexes est le duel de la famille des cubes, il y a donc encore des 3**n
articles à cataloguer. Ici, nous parcourons -1,0,1
toutes les dimensions et si la coordonnée non nulle d'un sommet est égale à la coordonnée correspondante du point, le sommet est ajouté à la liste correspondant à ce point. Ainsi, une arête correspond à un point à deux coordonnées non nulles, un triangle à un point à 3 coordonnées non nulles et un tétraèdre à un point à 4 contacts non nuls (dans l'espace 4d).
Les tableaux de sommets résultants pour chaque point sont stockés dans un grand tableau comme pour les autres cas, et nous devons supprimer tous les éléments avec moins de 2 sommets avant de revenir. Mais dans ce cas, nous n'avons pas à supprimer le parent complet n-tope car l'algorithme ne l'enregistre pas.
Les implémentations du code du cube ont été conçues pour être aussi similaires que possible. Bien que cela ait une certaine élégance, il est probable que des algorithmes plus efficaces basés sur les mêmes principes pourraient être conçus.
https://en.wikipedia.org/wiki/Hypercube
http://mathworld.wolfram.com/Hypercube.html
https://en.wikipedia.org/wiki/Cross-polytope
http://mathworld.wolfram.com/CrossPolytope.html
Code de génération de tableaux pour les cas spéciaux 3D
Une orientation avec l'icosaèdre / dodécaèdre orienté avec le quintuple axe de symétrie parallèle à la dernière dimension a été utilisée, car elle permettait l'étiquetage le plus cohérent des pièces. La numérotation des sommets et des faces pour l'icosaèdre est selon le diagramme dans les commentaires de code, et inversée pour le dodécaèdre.
Selon https://en.wikipedia.org/wiki/Regular_icosahedron, la latitude des 10 sommets non polaires de l'icosaèdre est de +/- arctan (1/2) Les coordonnées des 10 premiers sommets de l'icosaèdre sont calculées à partir de ceci, sur deux cercles de rayon 2 à distance +/- 2 du plan xy. Cela rend le rayon global de la circonscription sqrt (5) donc les 2 derniers sommets sont à (0,0, + / - sqrt (2)).
Les coordonnées des sommets du dodécaèdre sont calculées en additionnant les coordonnées des trois sommets de l'icosaèdre qui les entourent.
=begin
TABLE NAMES vertices edges faces
icosahedron vv ee ff
dodecahedron uu aa bb
10
/ \ / \ / \ / \ / \
/10 \ /12 \ /14 \ /16 \ /18 \
-----1-----3-----5-----7-----9
\ 0 / \ 2 / \ 4 / \ 6 / \ 8 / \
\ / 1 \ / 3 \ / 5 \ / 7 \ / 9 \
0-----2-----4-----6-----8-----
\11 / \13 / \15 / \17 / \19 /
\ / \ / \ / \ / \ /
11
=end
vv=[];ee=[];ff=[]
10.times{|i|
vv[i]=[2*sin(PI/5*i),2*cos(PI/5*i),(-1)**i]
ee[i]=[i,(i+1)%10];ee[i+10]=[i,(i+2)%10];ee[i+20]=[i,11-i%2]
ff[i]=[(i-1)%10,i,(i+1)%10];ff[i+10]=[(i-1)%10,10+i%2,(i+1)%10]
}
vv+=[[0,0,-5**0.5],[0,0,5**0.5]]
uu=[];aa=[];bb=[]
10.times{|i|
uu[i]=(0..2).map{|j|vv[ff[i][0]][j]+vv[ff[i][1]][j]+vv[ff[i][2]][j]}
uu[i+10]=(0..2).map{|j|vv[ff[i+10][0]][j]+vv[ff[i+10][1]][j]+vv[ff[i+10][2]][j]}
aa[i]=[i,(i+1)%10];aa[i+10]=[i,(i+10)%10];aa[i+20]=[(i-1)%10+10,(i+1)%10+10]
bb[i]=[(i-1)%10+10,(i-1)%10,i,(i+1)%10,(i+1)%10+10]
}
bb+=[[10,12,14,16,18],[11,13,15,17,19]]
Code de génération des tableaux pour les cas spéciaux 4d
C'est un peu un hack. Ce code prend quelques secondes pour s'exécuter. Il serait préférable de stocker la sortie dans un fichier et de la charger au besoin.
La liste des 120 coordonnées de sommet pour la 600cell provient de http://mathworld.wolfram.com/600-Cell.html . Les 24 coordonnées de sommets qui ne présentent pas de nombre d'or forment les sommets d'un 24 cellules. Wikipedia a le même schéma mais a une erreur dans l'échelle relative de ces 24 coordonnées et des 96 autres.
#TABLE NAMES vertices edges faces cells
#600 cell (analogue of icosahedron) v e f g
#120 cell (analogue of dodecahedron) u x y z
#24 cell o p q r
#600-CELL
# 120 vertices of 600cell. First 24 are also vertices of 24-cell
v=[[2,0,0,0],[0,2,0,0],[0,0,2,0],[0,0,0,2],[-2,0,0,0],[0,-2,0,0],[0,0,-2,0],[0,0,0,-2]]+
(0..15).map{|j|[(-1)**(j/8),(-1)**(j/4),(-1)**(j/2),(-1)**j]}+
(0..95).map{|i|j=i/12
a,b,c,d=1.618*(-1)**(j/4),(-1)**(j/2),0.618*(-1)**j,0
h=[[a,b,c,d],[b,a,d,c],[c,d,a,b],[d,c,b,a]][i%12/3]
(i%3).times{h[0],h[1],h[2]=h[1],h[2],h[0]}
h}
#720 edges of 600cell. Identified by minimum distance of 2/phi between them
e=[]
120.times{|i|120.times{|j|
e<<[i,j] if i<j && ((v[i][0]-v[j][0])**2+(v[i][1]-v[j][1])**2+(v[i][2]-v[j][2])**2+(v[i][3]-v[j][3])**2)**0.5<1.3
}}
#1200 faces of 600cell.
#If 2 edges share a common vertex and the other 2 vertices form an edge in the list, it is a valid triangle.
f=[]
720.times{|i|720.times{|j|
f<< [e[i][0],e[i][1],e[j][1]] if i<j && e[i][0]==e[j][0] && e.index([e[i][1],e[j][1]])
}}
#600 cells of 600cell.
#If 2 triangles share a common edge and the other 2 vertices form an edge in the list, it is a valid tetrahedron.
g=[]
1200.times{|i|1200.times{|j|
g<< [f[i][0],f[i][1],f[i][2],f[j][2]] if i<j && f[i][0]==f[j][0] && f[i][1]==f[j][1] && e.index([f[i][2],f[j][2]])
}}
#120 CELL (dual of 600 cell)
#600 vertices of 120cell, correspond to the centres of the cells of the 600cell
u=g.map{|i|s=[0,0,0,0];i.each{|j|4.times{|k|s[k]+=v[j][k]/4.0}};s}
#1200 edges of 120cell at centres of faces of 600-cell. Search for pairs of tetrahedra with common face
x=f.map{|i|s=[];600.times{|j|s<<j if i==(i & g[j])};s}
#720 pentagonal faces, surrounding edges of 600-cell. Search for sets of 5 tetrahedra with common edge
y=e.map{|i|s=[];600.times{|j|s<<j if i==(i & g[j])};s}
#120 dodecahedral cells surrounding vertices of 600-cell. Search for sets of 20 tetrahedra with common vertex
z=(0..119).map{|i|s=[];600.times{|j|s<<j if [i]==([i] & g[j])};s}
#24-CELL
#24 vertices, a subset of the 600cell
o=v[0..23]
#96 edges, length 2, found by minimum distances between vertices
p=[]
24.times{|i|24.times{|j|
p<<[i,j] if i<j && ((v[i][0]-v[j][0])**2+(v[i][1]-v[j][1])**2+(v[i][2]-v[j][2])**2+(v[i][3]-v[j][3])**2)**0.5<2.1
}}
#96 triangles
#If 2 edges share a common vertex and the other 2 vertices form an edge in the list, it is a valid triangle.
q=[]
96.times{|i|96.times{|j|
q<< [p[i][0],p[i][1],p[j][1]] if i<j && p[i][0]==p[j][0] && p.index([p[i][1],p[j][1]])
}}
#24 cells. Calculates the centre of the cell and the 6 vertices nearest it
r=(0..23).map{|i|a,b=(-1)**i,(-1)**(i/2)
c=[[a,b,0,0],[a,0,b,0],[a,0,0,b],[0,a,b,0],[0,a,0,b],[0,0,a,b]][i/4]
s=[]
24.times{|j|t=v[j]
s<<j if (c[0]-t[0])**2+(c[1]-t[1])**2+(c[2]-t[2])**2+(c[3]-t[3])**2<=2
}
s}
https://en.wikipedia.org/wiki/600-cell
http://mathworld.wolfram.com/600-Cell.html
https://en.wikipedia.org/wiki/120-cell
http://mathworld.wolfram.com/120-Cell.html
https://en.wikipedia.org/wiki/24-cell
http://mathworld.wolfram.com/24-Cell.html
Exemple d'utilisation et de sortie
cell24 = polytope[[3,4,3]]
puts "vertices"
cell24[0].each{|i|p i}
puts "edges"
cell24[1].each{|i|p i}
puts "faces"
cell24[2].each{|i|p i}
puts "cells"
cell24[3].each{|i|p i}
vertices
[2, 0, 0, 0]
[0, 2, 0, 0]
[0, 0, 2, 0]
[0, 0, 0, 2]
[-2, 0, 0, 0]
[0, -2, 0, 0]
[0, 0, -2, 0]
[0, 0, 0, -2]
[1, 1, 1, 1]
[1, 1, 1, -1]
[1, 1, -1, 1]
[1, 1, -1, -1]
[1, -1, 1, 1]
[1, -1, 1, -1]
[1, -1, -1, 1]
[1, -1, -1, -1]
[-1, 1, 1, 1]
[-1, 1, 1, -1]
[-1, 1, -1, 1]
[-1, 1, -1, -1]
[-1, -1, 1, 1]
[-1, -1, 1, -1]
[-1, -1, -1, 1]
[-1, -1, -1, -1]
edges
[0, 8]
[0, 9]
[0, 10]
[0, 11]
[0, 12]
[0, 13]
[0, 14]
[0, 15]
[1, 8]
[1, 9]
[1, 10]
[1, 11]
[1, 16]
[1, 17]
[1, 18]
[1, 19]
[2, 8]
[2, 9]
[2, 12]
[2, 13]
[2, 16]
[2, 17]
[2, 20]
[2, 21]
[3, 8]
[3, 10]
[3, 12]
[3, 14]
[3, 16]
[3, 18]
[3, 20]
[3, 22]
[4, 16]
[4, 17]
[4, 18]
[4, 19]
[4, 20]
[4, 21]
[4, 22]
[4, 23]
[5, 12]
[5, 13]
[5, 14]
[5, 15]
[5, 20]
[5, 21]
[5, 22]
[5, 23]
[6, 10]
[6, 11]
[6, 14]
[6, 15]
[6, 18]
[6, 19]
[6, 22]
[6, 23]
[7, 9]
[7, 11]
[7, 13]
[7, 15]
[7, 17]
[7, 19]
[7, 21]
[7, 23]
[8, 9]
[8, 10]
[8, 12]
[8, 16]
[9, 11]
[9, 13]
[9, 17]
[10, 11]
[10, 14]
[10, 18]
[11, 15]
[11, 19]
[12, 13]
[12, 14]
[12, 20]
[13, 15]
[13, 21]
[14, 15]
[14, 22]
[15, 23]
[16, 17]
[16, 18]
[16, 20]
[17, 19]
[17, 21]
[18, 19]
[18, 22]
[19, 23]
[20, 21]
[20, 22]
[21, 23]
[22, 23]
faces
[0, 8, 9]
[0, 8, 10]
[0, 8, 12]
[0, 9, 11]
[0, 9, 13]
[0, 10, 11]
[0, 10, 14]
[0, 11, 15]
[0, 12, 13]
[0, 12, 14]
[0, 13, 15]
[0, 14, 15]
[1, 8, 9]
[1, 8, 10]
[1, 8, 16]
[1, 9, 11]
[1, 9, 17]
[1, 10, 11]
[1, 10, 18]
[1, 11, 19]
[1, 16, 17]
[1, 16, 18]
[1, 17, 19]
[1, 18, 19]
[2, 8, 9]
[2, 8, 12]
[2, 8, 16]
[2, 9, 13]
[2, 9, 17]
[2, 12, 13]
[2, 12, 20]
[2, 13, 21]
[2, 16, 17]
[2, 16, 20]
[2, 17, 21]
[2, 20, 21]
[3, 8, 10]
[3, 8, 12]
[3, 8, 16]
[3, 10, 14]
[3, 10, 18]
[3, 12, 14]
[3, 12, 20]
[3, 14, 22]
[3, 16, 18]
[3, 16, 20]
[3, 18, 22]
[3, 20, 22]
[4, 16, 17]
[4, 16, 18]
[4, 16, 20]
[4, 17, 19]
[4, 17, 21]
[4, 18, 19]
[4, 18, 22]
[4, 19, 23]
[4, 20, 21]
[4, 20, 22]
[4, 21, 23]
[4, 22, 23]
[5, 12, 13]
[5, 12, 14]
[5, 12, 20]
[5, 13, 15]
[5, 13, 21]
[5, 14, 15]
[5, 14, 22]
[5, 15, 23]
[5, 20, 21]
[5, 20, 22]
[5, 21, 23]
[5, 22, 23]
[6, 10, 11]
[6, 10, 14]
[6, 10, 18]
[6, 11, 15]
[6, 11, 19]
[6, 14, 15]
[6, 14, 22]
[6, 15, 23]
[6, 18, 19]
[6, 18, 22]
[6, 19, 23]
[6, 22, 23]
[7, 9, 11]
[7, 9, 13]
[7, 9, 17]
[7, 11, 15]
[7, 11, 19]
[7, 13, 15]
[7, 13, 21]
[7, 15, 23]
[7, 17, 19]
[7, 17, 21]
[7, 19, 23]
[7, 21, 23]
cells
[0, 1, 8, 9, 10, 11]
[1, 4, 16, 17, 18, 19]
[0, 5, 12, 13, 14, 15]
[4, 5, 20, 21, 22, 23]
[0, 2, 8, 9, 12, 13]
[2, 4, 16, 17, 20, 21]
[0, 6, 10, 11, 14, 15]
[4, 6, 18, 19, 22, 23]
[0, 3, 8, 10, 12, 14]
[3, 4, 16, 18, 20, 22]
[0, 7, 9, 11, 13, 15]
[4, 7, 17, 19, 21, 23]
[1, 2, 8, 9, 16, 17]
[2, 5, 12, 13, 20, 21]
[1, 6, 10, 11, 18, 19]
[5, 6, 14, 15, 22, 23]
[1, 3, 8, 10, 16, 18]
[3, 5, 12, 14, 20, 22]
[1, 7, 9, 11, 17, 19]
[5, 7, 13, 15, 21, 23]
[2, 3, 8, 12, 16, 20]
[3, 6, 10, 14, 18, 22]
[2, 7, 9, 13, 17, 21]
[6, 7, 11, 15, 19, 23]