Рубин
Фон
Есть три семейства правильных многогранников, простирающихся в бесконечные измерения:
симплексы, членом которых является тетраэдр (я буду часто называть их здесь гипертетраэдрами, хотя термин симплекс более правильный.) Их символы Шлафи имеют вид {3,3,...,3,3}
n-кубы, членом которых является куб. Их символы Schlafi имеют форму{4,3,...,3,3}
ортоплексы, членом которых является октаэдр (я буду часто называть их здесь гипероктаэдрами). Их символы Шлафи имеют вид {3,3,...,3,4}
Существует еще одно бесконечное семейство правильных многогранников, символ {m}
, из двумерных многоугольников, которые могут иметь любое количество ребер m.
В дополнение к этому есть только пять других частных случаев правильного многогранника: трехмерный икосаэдр {3,5}
и додекаэдр {5,3}
; их 4-мерные аналоги - 600-элементная {3,3,5}
и 120-элементная {5,3,3}
; и еще один 4-мерный многогранник, 24-элементный {3,4,3}
(его ближайшими аналогами в 3-х измерениях являются кубоктаэдр и его двойной ромбический додекаэдр.)
Основная функция
Ниже приведена основная polytope
функция, которая интерпретирует символ Шлафи. Он ожидает массив чисел и возвращает массив, содержащий несколько массивов, следующим образом:
Массив всех вершин, каждая из которых выражается в виде n-элементного массива координат (где n - количество измерений)
Массив всех ребер, каждое из которых выражается в виде 2-х элементов индексов вершин
Массив всех граней, каждая из которых выражается как m-элемент индексов вершин (где m - количество вершин на грани)
и так далее в зависимости от количества измерений.
Он сам вычисляет 2d многогранники, вызывает функции для трех бесконечномерных семейств и использует таблицы поиска для пяти особых случаев. Он ожидает найти функции и таблицы, объявленные выше.
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
}
Функции для семейств тетраэдров, кубов и октаэдров
https://en.wikipedia.org/wiki/Simplex
https://en.wikipedia.org/wiki/5-cell (4-й симплекс)
http://mathworld.wolfram.com/Simplex.html
Объяснение семейства тетраэдров - координаты
n-мерный симплекс / гипертетраэдр имеет n + 1 точек. Очень легко дать вершины n-мерного симплекса в n + 1 измерениях.
Таким образом, (1,0,0),(0,1,0),(0,0,1)
описывается двумерный треугольник, встроенный в 3 измерения, и (1,0,0,0),(0,1,0,0),(0,0,1,0),(0,0,0,1)
описывается трехмерный тетраэдр, встроенный в 4 измерения. Это легко проверить, подтвердив, что все расстояния между вершинами являются sqrt (2).
Различные сложные алгоритмы приведены в Интернете для поиска вершин для n-мерного симплекса в n-мерном пространстве. Я нашел удивительно простой ответ в комментариях Уилла Джейги к этому ответу /mathpro//a/38725 . Последняя точка лежит на прямой p=q=...=x=y=z
на расстоянии sqrt (2) от остальных. Таким образом, указанный выше треугольник можно преобразовать в тетраэдр, добавив точку в точке (-1/3,-1/3,-1/3)
или (1,1,1)
. Эти 2 возможных значения координат для последней точки задаются (1-(1+n)**0.5)/n
и(1+(1+n)**0.5)/n
Поскольку вопрос говорит о том, что размер n-вершины не имеет значения, я предпочитаю умножать на n и использовать координаты с (n,0,0..0)
точностью до (0..0,0,n)
конечной точки, (t,t,..,t,t)
где t = 1-(1+n)**0.5
для простоты.
Поскольку центр этого тетраэдра не находится в начале координат, исправление всех координат должно быть выполнено линией, s.map!{|j|j-((1-(1+n)**0.5)+n)/(1+n)}
которая определяет, как далеко находится центр от начала координат, и вычитает его. Я сохранил это как отдельную операцию. Тем не менее, я использовал « s[i]+=n
где» s[i]=n
, чтобы сослаться на тот факт, что, когда массив инициализируется, s=[0]*n
мы можем вместо этого поместить правильное смещение и выполнить коррекцию центрирования в начале, а не в конце.
Объяснение семейства тетраэдров - топология графа
Граф симплекса является полным графом: каждая вершина связана ровно один раз с каждой другой вершиной. Если у нас есть n симплекс, мы можем удалить любую вершину, чтобы получить n-1 симплекс, вплоть до точки, где у нас есть треугольник или даже ребро.
Таким образом, мы имеем в общей сложности 2 ** (n + 1) элементов для каталога, каждый из которых представлен двоичным числом. Это варьируется от всех 0
s для небытия, до одного 1
для вершины и двух 1
s для ребра, до всех 1
s для полного многогранника.
Мы создали массив пустых массивов для хранения элементов каждого размера. Затем мы выполняем цикл от нуля до (2 ** n + 1), чтобы сгенерировать каждое из возможных подмножеств вершин и сохранить их в массиве в соответствии с размером каждого подмножества.
Мы не заинтересованы ни в чем меньше, чем ребро (вершина или ноль), ни в полном многограннике (поскольку полный куб не приведен в примере в вопросе), поэтому мы возвращаемся, tg[2..n]
чтобы удалить эти нежелательные элементы. Прежде чем вернуться, мы прикрепляем [tv], содержащий координаты вершины, в начало.
код
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]}
объяснение семейств куба и октаэдра - координаты
П-куба имеет 2**n
вершины, каждый из которых представлено массивом п 1
с и -1
х (все возможности разрешены.) Мы итерация через индексы 0
к 2**n-1
списку всех вершин, и построить массив для каждой вершины перебором бит индекс и добавление -1
или 1
к массиву (младший значащий бит к старшему значащему биту). Таким образом, двоичный 1101
становится 4-й точкой [1,-1,1,1]
.
У n-октаэдра или n-ортоплекса есть 2n
вершины, причем все координаты равны нулю, кроме одной, которая может быть 1
или -1
. Порядок вершин в массиве вырабатываемого [[1,0,0..],[-1,0,0..],[0,1,0..],[0,-1,0..],[0,0,1..],[0,0,-1..]...]
. Обратите внимание, что, поскольку октаэдр является двойником куба, вершины октаэдра определяются центрами граней окружающего его куба.
объяснение семейств куба и октаэдра - топология графа
Некоторое вдохновение было взято со стороны гиперкуба и того факта, что гипероктаэдр является двойником гиперкуба.
Для n-cube есть 3**n
элементы для каталога. Например, куб 3 имеет 3**3
= 27 элементов. Это можно увидеть, изучив кубик Рубика, который имеет 1 центр, 6 граней, 12 ребер и 8 вершин, что в общей сложности составляет 27. Мы перебираем -1,0 и -1 во всех измерениях, определяя n-куб с длиной стороны 2x2x2 .. и вернуть все вершины, которые НЕ находятся на противоположной стороне куба. Таким образом, центральная точка куба возвращает все 2 ** n вершин, а перемещение на одну единицу от центра вдоль любой оси уменьшает количество вершин вдвое.
Как и в случае с семейством тетраэдров, мы начинаем с создания пустого массива массивов и заполняем его в соответствии с количеством вершин на элемент. Обратите внимание, что, поскольку число вершин изменяется как 2 ** n при прохождении ребер, граней, кубов и т. Д., Мы используем log2(s.size)+1
вместо простого s.size
. Опять же, мы должны удалить сам гиперкуб и все элементы с менее чем 2 вершинами, прежде чем вернуться из функции.
Семейство октаэдров / ортоплексов - это двойники семейства кубов, поэтому снова есть 3**n
элементы для каталога. Здесь мы повторяем -1,0,1
все измерения и, если ненулевая координата вершины равна соответствующей координате точки, вершина добавляется в список, соответствующий этой точке. Таким образом, ребро соответствует точке с двумя ненулевыми координатами, треугольнику - точке с 3 ненулевыми координатами и тетраэдру - точке с 4 ненулевыми контактами (в 4-мерном пространстве).
Полученные массивы вершин для каждой точки сохраняются в большом массиве, как и в других случаях, и мы должны удалить все элементы с менее чем 2 вершинами перед возвратом. Но в этом случае нам не нужно удалять полный родительский n-tope, потому что алгоритм его не записывает.
Реализации кода для куба должны быть максимально похожими. Хотя это имеет определенную элегантность, вполне вероятно, что могут быть разработаны более эффективные алгоритмы, основанные на тех же принципах.
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
Код для генерации таблиц для 3d особых случаев
Ориентация с икосаэдром / додекаэдром, ориентированным с пятикратной осью симметрии, параллельной последнему измерению, была использована для наиболее последовательной маркировки деталей. Нумерация вершин и граней для икосаэдра соответствует схеме в комментариях к коду и обратная для додекаэдра.
Согласно https://en.wikipedia.org/wiki/Regular_icosahedron широта 10 неполярных вершин икосаэдра равна +/- arctan (1/2). Координаты первых 10 вершин икосаэдра вычисляются из это на двух окружностях радиуса 2 на расстоянии +/- 2 от плоскости ху. Это делает общий радиус окружности sqrt (5), поэтому последние 2 вершины находятся в (0,0, + / - sqrt (2)).
Координаты вершин додекаэдра вычисляются путем суммирования координат трех окружающих их вершин икосаэдра.
=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]]
Код для генерации таблиц для 4-х особых случаев
Это немного взломать. Этот код занимает несколько секунд для запуска. Было бы лучше сохранить вывод в файл и загрузить его по мере необходимости.
Список из 120 координат вершин для 600cell взят с http://mathworld.wolfram.com/600-Cell.html . 24 координаты вершины, которые не имеют золотого сечения, образуют вершины из 24 ячеек. Википедия имеет ту же схему, но имеет ошибку в относительном масштабе этих 24 координат и других 96.
#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
Пример использования и вывода
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]