- PVSM.RU - https://www.pvsm.ru -

Как-то в гостях мне в руки попалась головоломка, в которой из 25 одинаковых фигурок требовалось собрать куб. Я провозился с ней почти весь вечер, и как можно догадаться, абсолютно безрезультатно. Тем не менее, я не мог сдаться просто так.
Не можешь сам — заставь компьютер. Сказано — сделано. В результате написанному по наитию алгоритму пришлось работать всю ночь, чтобы найти все 4 уникальных решения. В процессе гугления решений для сравнения, я нашёл программу Burr Tools [1], которая справилась с этой задачей за 3 минуты на моём ноутбуке.
Такая разница в скорости заставила меня разобраться, как решается эта задача и ещё целый класс подобных.


Данная головоломка является классическим представителем задачи о точном покрытии [2].
Представить формулировку задачи можно на следующем примере. Вы приходите в магазин за коллекционными картами. Какие колоды вам стоит купить, чтобы собрать полную коллекцию без повторяющихся карт?
Для такой задачи существует Алгоритм X Дональда Кнута [3]. Но прежде чем перейти к рассмотрению алгоритма давайте посмотрим, как свести нашу головоломку с составлением куба к задаче о точном покрытии.
Для простоты будем решать похожую задачу о складывании из четырёх плоских уголков уголка побольше. Если фигура сложена, то каждый квадратик фигуры принадлежит одному и только одному уголку, что, согласитесь, уже очень похоже звучит на формулировку задачи о полном покрытии.

Обозначим числами все квадратики большой фигуры, а буквами всевозможные положения маленького уголка внутри неё (здесь ради наглядности сознательно выкинуты некоторые «плохие» положения уголков, которые точно не могут присутствовать в решении). Совокупность 12 квадратиков — это как раз то, что мы будем пытаться покрыть положениями A-L. Задачу можно также представить в виде таблицы (матрицы инцидентности [4]). Столбцы будут соответствовать квадратикам, строки — положениям уголка, а единица на пересечении столбца и строки ставится в том случае, если соответствующее положение уголка покрывает соответствующий квадратик.

Чуть менее очевидным образом сводятся к задаче о полном покрытии головоломка, в которой все кусочки разные, или такая игра как судоку. Читателю предлагается самостоятельно придумать, как формализовать эти ситуации (если что, ответ [5] есть в википедии [6]).
Алгоритм X состоит их нескольких шагов, которые сводят задачу к исходной, но с меньшим количеством столбцов/строк. Причём надо отметить, что алгоритм ветвится на определённом шаге. Иными словами, это всё равно перебор, но умный :-)

Выбор столбца на геометрическом языке означает выбор какого-нибудь ещё не покрытого квадратика. В нашем случае это будет квадратик 24.

Квадратик 24 может закрываться как положением A, так и положением D. У нас нет предпосылок выбрать то или другое, поэтому мы отдельно рассматриваем оба варианта. Это шаг, на котором осуществляется перебор вариантов. Чтобы уменьшить их число, часто на шаге 1 выбирают не произвольный столбец, а столбец, содержащий наименьшее число единиц.

Выберем строку A и внесём её в наш стек решения. Теперь нам нужно выделить все квадратики, которые занимает положение A.

Все строки, которые имеют ненулевые элементы в выбранных столбцах, уже точно не могут участвовать в построении этой ветки решения. Другими словами, мы избавляемся от всех положений уголка, которые пересекаются (имеют общие квадратики) с положением A. Более того, поскольку квадратики 24, 33, 34 мы уже покрыли (строка А уже сидит в стеке), то нам не нужно о них заботиться дальше, и соответствующие столбцы тоже удаляются.

После удаления строк и столбцов мы получили меньшую таблицу. А с точки зрения геометрии мы получили задачу о покрытии меньшей фигуры, причём оставшиеся строчки автоматически соответствуют возможным положениям уголков в этой фигуре.
После перехода к шагу 1, у нас могут возникнуть особые ситуации.
Ситуация 1. Мы не можем выбрать столбец, потому что столбцов в матрице не осталось. Это означает, что мы покрыли все имевшиеся квадратики, следовательно, мы нашли решение задачи. Содержимое стека можно добавить к списку найденных решений.
Ситуация 2. Мы выбрали столбец, но в нём нет ни одной единицы, как например, у столбца 44 на последней картинке. Это означает, что для выбранного квадратика не осталось ни одного положения уголка, который бы мог его покрыть. Следовательно, эта ветвь не имеет решения. Частный случай такой ситуации — в матрице не осталось строк. Стоит заметить, что выбирая на шаге 1 столбец с наименьшим числом единиц, мы всегда получим пустой столбец, как только он появится.
Поскольку за итерацию алгоритма мы заведомо избавляемся от одной строки и одного столбца, количество строк и столбцов уменьшается, и рано или поздно мы придём к ситуации 1 или ситуации 2.

Для быстрой работы алгоритма необходимо уметь быстро удалять строки и столбцы из матрицы. Если хранить матрицу в виде двумерного массива, то это достаточно проблематично. С другой стороны, получаемая матрица практически всегда очень разреженная [7] (в частности матрица для задачи с пентамино содержит всего 4 % единиц), и поэтому удобно представлять её в виде двумерного двусвязного списка. Такое представление позволяет нам за минимальное время получать все ненулевые элементы строки или столбца.
Наконец, имея в своём распоряжении такой список, очень удобно удалять столбцы и строки, а возвращаясь по ветке назад, восстанавливать их.
// Удаление элемента
node->next->prev = node->prev;
node->prev->next = node->next;
// Восстановление элемента
node->next->prev = node;
node->prev->next = node;
Именно на этой идее и основан метод танцующих ссылок [8] Кнута. (Кстати, если ввести в гугл название этого метода в единственном числе «dancing link», то в картинках будет много гифок с главным героем Legend of Zelda.)
Окончательный код решения исходной головоломки можно посмотреть на гитхабе [9] (большую часть кода занимает подготовка матрицы и функции проверки решений на идентичность). На моём ноутбуке программа за 5 секунд находит 4 уникальных решения (не приводимых друг к другу поворотами и отражениями) и в течение ещё 50 секунд убеждается, что других нет. Было приятно осознать, что твоя реализация работает быстрее, чем решение Burr Tools.

Одна из оптимизаций алгоритма заключалась в том, чтобы оставить только 2 принципиальных положения пентаминошки, закрывающих кубик (0, 0, 0), так как остальные её возможные положения являются поворотами и отражениями. Это уменьшало количество вариантов перебора в 6 раз. (На самом деле, достаточно оставить одно положение, но это нуждается в дополнительном обосновании).
Решение этой (и других головоломок) можно найти на сайте puzzlewillbeplayed.com [10]. А тем, кто дочитал до конца, — бонус в виде визуализации кусочка работы алгоритма. Возможно, этой анимацией будет оправдано прилагательное «танцующий» в названии алгоритма.
Автор: p53
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/algoritmy/43760
Ссылки в тексте:
[1] Burr Tools: http://burrtools.sourceforge.net/
[2] задачи о точном покрытии: http://en.wikipedia.org/wiki/Exact_cover_problem
[3] Алгоритм X Дональда Кнута: http://en.wikipedia.org/wiki/Knuth%27s_Algorithm_X
[4] матрицы инцидентности: http://en.wikipedia.org/wiki/Incidence_matrix
[5] ответ: http://en.wikipedia.org/wiki/Exact_cover#Pentomino_tiling
[6] википедии: http://en.wikipedia.org/wiki/Exact_cover#Sudoku
[7] разреженная: http://en.wikipedia.org/wiki/Sparse_array
[8] танцующих ссылок: http://en.wikipedia.org/wiki/Dancing_Links
[9] гитхабе: https://github.com/yfp/dlx25N/blob/master/code.c
[10] puzzlewillbeplayed.com: http://puzzlewillbeplayed.com/Pentominoes/N-5x5x5.html
[11] Источник: http://habrahabr.ru/post/194410/
Нажмите здесь для печати.