Решение Головоломки Галакуба На Python

На Новый год я купила племяннику пазл Галакуб.

Задача — собрать из различных частей кубик 4х4х4. Общий объём деталей ровно 4х4х4. Прежде чем дарить, нужно было собрать пазл.

Красивое симметричное решение было найдено довольно быстро.

Но я задавался вопросом, было ли это единственным решением или нет. Интуиция подсказывала, что это единственное, но мне хотелось проверить.



Решение головоломки Галакуба на Python

Я решил быстро написать скрипт, чтобы попробовать все варианты.

В идеале нужно было сделать это до новогоднего выступления Путина.

Ситуацию усугубляло то, что код был написан на MacBook моих родителей.

Поставить на него какие-то библиотеки — задача посложнее, чем написать саму программу.

Код получился на удивление красивым и понятным.

Это удобно объяснить.

Возможно текст будет полезен, например, изучающим Python. Все части были представлены в виде тензоров 4х4х4.

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
   

def zero_vector(): return [0] * 4 def zero_matrix(): return [zero_vector() for _ in xrange(4)] def zero_tensor(): return [zero_matrix() for _ in xrange(4)]

Куб обозначен буквой «Q», угол — буквой «J», а закорючка — буквой «Z».



Решение головоломки Галакуба на Python



def parse_tensor(data): tensor = zero_tensor() lines = data.splitlines() for z in xrange(2): for y in xrange(4): line = lines[z * 5 + 1 + y] for x in xrange(4): if line[x] == '*': tensor[z][y][x] = 1 return tensor J = parse_tensor(""" ***.

*.

.

.

***.

*.

.

.

""") Q = parse_tensor(""" **.

**.

.

.

**.

**.

.

.

""") Z = parse_tensor(""" *.

*.

.

.

*.

***.

.

**.

.

""") >>> J [[[1, 1, 1, 0], [1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[1, 1, 1, 0], [1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]]

Чтобы было удобнее смотреть на тензоры (а смотреть на них нужно будет много и внимательно), функция show_tensor была написана как инверсия функции parse_tensor:

def show_tensor(tensor): for y in xrange(4): for z in xrange(4): for x in xrange(4): value = tensor[z][y][x] print { 1: '*', 0: '.

' }[value], print ' ', print def show_tensors(tensors): for tensor in tensors: show_tensor(tensor) print >>> show_tensors([J, Q, Z]) * * * .

* * * .

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

* .

.

.

* * * .

.

.

.

.

.

.

.

.

.

.

.

.

.

* * .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.



Далее необходимо было сгенерировать все возможные позиции для каждой детали.

Поворот по оси X и Y на 90 градусов сводится к перестановке осей.



Решение головоломки Галакуба на Python



def rotate_by_x(tensor): rotated = zero_tensor() for z in xrange(4): for y in xrange(4): for x in xrange(4): rotated[z][y][x] = tensor[y][-z - 1][x] return rotated def rotate_by_y(tensor): rotated = zero_tensor() for z in xrange(4): for y in xrange(4): for x in xrange(4): rotated[z][y][x] = tensor[x][y][-z - 1] return rotated

Чтобы не дублировать циклы, можно создать функцию Transform_tensor, она еще не раз пригодится:

def transform_tensor(tensor, function): transformed = zero_tensor() for z in xrange(4): for y in xrange(4): for x in xrange(4): transformed[z][y][x] = function(tensor, x, y, z) return transformed def rotate_by_x(tensor): return transform_tensor(tensor, lambda _, x, y, z: _[y][-z - 1][x]) def rotate_by_y(tensor): return transform_tensor(tensor, lambda _, x, y, z: _[x][y][-z - 1])

Давай посмотрим что происходит:

def apply_transformation(tensor, transformation, times=1): for _ in xrange(times): tensor = transformation(tensor) return tensor def show_transformation(tensor, transformation): show_tensors([ tensor, transformation(tensor), apply_transformation(tensor, transformation, times=2), apply_transformation(tensor, transformation, times=3), apply_transformation(tensor, transformation, times=4), ]) >>> show_transformation(J, rotate_by_x) * * * .

* * * .

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* .

.

.

* * * .

.

.

.

.

.

.

.

.

* .

.

.

* * * .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

* * * .

* * * .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * * .

* .

.

.

.

.

.

.

.

.

.

.

* * * .

* .

.

.

.

.

.

.

.

.

.

.

* * * .

* * * .

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

>>> show_transformation(J, rotate_by_y) * * * .

* * * .

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

.

.

.

.

* * .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * * .

* * * .

.

.

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

* * .

.

.

.

.

.

* * .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * * .

* * * .

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.



Вращение вдоль оси Z неожиданно может быть достигнуто путем вращения вдоль осей X и Y. Вам просто нужно вращать в разные стороны, поэтому вам нужно ввести направление в Rotate_by_y:

def rotate_by_y(tensor, direction=1): if direction == 1: function = lambda _, x, y, z: _[x][y][-z - 1] else: function = lambda _, x, y, z: _[-x - 1][y][z] return transform_tensor(tensor, function) def rotate_by_z(tensor): return rotate_by_y(rotate_by_x(rotate_by_y(tensor, direction=-1)))

Давай посмотрим что происходит:

Решение головоломки Галакуба на Python



>>> show_transformation(J, rotate_by_z) * * * .

* * * .

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

* * * .

* * * .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

* * * .

* * * .

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.



Ну кроме ротаций есть еще и сдвиги.

Имея функцию Transform_tensor, сделать трансляционный сдвиг очень просто:

def shift_by_x(tensor): return transform_tensor(tensor, lambda _, x, y, z: _[z][y][(x + 1) % 4])

Проблема только в том, что возникают несуществующие детали:

>>> show_transformation(J, shift_by_x) * * * .

* * * .

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * .

* * * .

* .

.

.

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* .

* * * .

* * .

.

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * * .

* * * .

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * * .

* * * .

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.



Поэтому решение должно быть сложным.

Смену мы сделаем только в том случае, если для переноса останется пустое место.

Вам нужно добавить код для расчета проекции тензора и проверки, пуста ли матрица:

def project_tensor(tensor, function): projection = zero_matrix() for i in xrange(4): for j in xrange(4): projection[i][j] = function(tensor, i, j) return projection def project_by_x(tensor): return project_tensor(tensor, lambda _, z, y: tensor[z][y][0]) def project_by_y(tensor): return project_tensor(tensor, lambda _, z, x: tensor[z][0][x]) def project_by_z(tensor): return project_tensor(tensor, lambda _, y, x: tensor[0][y][x]) def is_empty_matrix(matrix): for i in xrange(4): for j in xrange(4): if matrix[i][j]: return False return True

Теперь смена будет выглядеть так:

def shift_by_x(tensor): if is_empty_matrix(project_by_x(tensor)): return transform_tensor(tensor, lambda _, x, y, z: _[z][y][(x + 1) % 4]) return tensor

Теперь, если деталь достигает границы, ничего не происходит:

>>> show_transformation(J, shift_by_x) * * * .

* * * .

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * * .

* * * .

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * * .

* * * .

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * * .

* * * .

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * * .

* * * .

.

.

.

.

.

.

.

.

* .

.

.

* .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.



Правда, сгенерировать все возможные детали не получится.

Вам нужно добавить еще одно направление и каждый раз делать сдвиги в обе стороны.

Окончательный вариант есть на Github. Что ж, чтобы получить все возможные положения для каждой части, вам нужно перебрать все углы поворота и все размеры смещения:

def generate_permutations(tensor): for x_rotations in xrange(4): for y_rotations in xrange(4): for z_rotations in xrange(4): for x_shifts in xrange(3): for x_direction in (-1, 1): for y_shifts in xrange(3): for y_direction in (-1, 1): for z_shifts in xrange(3): for z_direction in (-1, 1): permutation = apply_transformation(tensor, rotate_by_x, times=x_rotations) permutation = apply_transformation(permutation, rotate_by_y, times=y_rotations) permutation = apply_transformation(permutation, rotate_by_z, times=z_rotations) permutation = apply_transformation(permutation, shift_by_x, direction=x_direction, times=x_shifts) permutation = apply_transformation(permutation, shift_by_y, direction=y_direction, times=y_shifts) permutation = apply_transformation(permutation, shift_by_z, direction=z_direction, times=z_shifts) yield permutation

Многие комбинации дублируются.

Например, бесполезно вращать куб; он не добавляет новые комбинации:

>>> Qs = list(generate_permutations(Q)) >>> show_tensors(sample(Qs, 10)) * * .

.

* * .

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.



Чтобы оставить только уникальные параметры, вам нужно определить функцию, которая присваивает номер тензору.

Для этого можно представить тензор 4х4х4 как двоичное 64-битное число:

def hash_tensor(tensor): hash = 0 index = 0 for z in xrange(4): for y in xrange(4): for x in xrange(4): index += 1 hash += tensor[z][y][x] * 2 ** index return hash

Теперь оставлять уникальные комбинации легко:

def unique_tensors(tensors): hashes = set() for tensor in tensors: hash = hash_tensor(tensor) if hash not in hashes: yield tensor hashes.add(hash) Zs = list(unique_tensors(generate_permutations(Z))) Js = list(unique_tensors(generate_permutations(J))) Qs = list(unique_tensors(generate_permutations(Q))) >>> show_tensors(sample(Qs, 10)) .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

* * .

.

* * .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.



Ввиду того, что кубик небольшой, вариантов не так много:

>>> len(Zs), len(Js), len(Qs) (288, 432, 27)

Самый примитивный поиск решения мог бы выглядеть так:

def solve(Zs, Js, Qs): for Z1 in Zs: for Z2 in Zs: for Z3 in Zs: for J1 in Js: for J2 in Js: for J3 in Js: for Q1 in Qs: for Q2 in Qs: if not tensors_intersect(Z1, Z2, Z3, J1, J2, J3, Q1, Q2): yield Z1, Z2, Z3, J1, J2, J3, Q1, Q2

Но это слишком глупо, вам нужно будет пройти 288 3 432 3 27 2 параметры.

Выход есть.

После того, как, например, первая часть закреплена, нужно игнорировать все варианты, в которых вторая часть пересекается с первой.

В случае с лобовым решением это не так.

Даже если Z1 и Z2 пересекутся, все остальные 6 подциклов будут работать.

Лучшее решение будет выглядеть так:

def solve_(path, done, hashes, todo): for hash in hashes: if not tensors_intersect(done, hash): if todo: for solution in solve_(path + [hash], union_hashes(done, hash), todo[0], todo[1:]): yield solution else: yield path + [hash] def solve(Zs, Js, Qs): done = zero_tensor() todo = [Z_hashes] * 3 + [J_hashes] * 3 + [Q_hashes] * 2 for solution in solve_([], done, todo[0], todo[1:]): yield solution

Но есть еще один нюанс.

Функция tensors_intersect выглядит следующим образом:

def tensors_intersect(a, b): for z in xrange(4): for y in xrange(4): for x in xrange(4):

Теги: #галакуб #python #python #программирование #Функциональное программирование

Вместе с данным постом часто просматривают: