Clojure: Решаем японские кроссворды с помощью core.logic
Я никогда не был сторонником одной единственной парадигмы программирования. Мне всегда нравилось изучать новые идеи и учиться комбинировать их с изученными ранее. Сейчас я решил немного поэкспериментировать с логическим программированием (или программированием в ограничениях). В качестве задачки для эксперимента я возьмусь решать японские кроссворды (nonogram). А реализую я ее с помощью библиотеки core.logic для языка Clojure.
Приготовления
Для начала сгенерируем новый проект с помощью Leiningen:
1
$ lein new nonograms-solver
Опишем информацию о нашем проекте в файле project.clj:
123456789
(defproject nonograms-solver"0.1.0-SNAPSHOT":description"Nonograms solver powered by clojure.core.logic":url"https://github.com/sviridov/nonograms-solver":license{:name"The MIT License":url"http://opensource.org/licenses/MIT"}:dependencies[[org.clojure/clojure"1.5.1"][org.clojure/core.logic"0.8.8"]]:aot:all:mainnonograms-solver.main)
Форматом выходных данных будет последовательность строк, где строка – это последовательность нолей и единиц (0 – пусто, 1 – закрашено). Реализуем вывод данных:
Сразу хочу отметить, что это не самая серьезная программа и я не ставлю здесь приоритет на производительность. Для меня важно решить эту задачу максимально лаканично.
Поместим логику решения кроссворда в файл core.clj:
Итак, как же мы будем искать решение нашего кроссворда? Никак! Главный вопрос в логическом программирование не в том “Как найти решение”, а в том “Что такое решение”. Дать ответ на этот вопрос мы можем с помощью ограничений, накладываемых на решение. Какие ограничения мы можем наложить на решение японского кроссворда?
Решение – это набор ячеек (клеток).
Ячейка может быть закрашена или пуста.
Все ячейки поделены на ряды и колонки.
Каждой линии (ряду или колонке) ставится в соответствие свой шаблон (подсказка).
Содержимое линии должно соответствовать своему шаблону согласно правилам японского кроссворда.
Определим функцию, в которой отобразим описанные выше правила:
(defn solve[{vertical-hints:verticalhorizontal-hints:horizontal}](let [grid-width(count vertical-hints)grid-height(count horizontal-hints)grid-size(* grid-widthgrid-height);; Набор логических переменных;; Каждая переменная это ячейка кроссвордаcells(repeatedlygrid-sizelvar);; Строки кроссворда (состоят из логических переменных)rows(partitiongrid-widthcells);; Столбцы кроссворда (состоят из логических переменных)columns(apply map vector rows)](->>(run1[result]; Запускаем поиск решения.; Ищем до тех пор, пока не найдем одно;; Решение - это набор ячеек(== resultcells);; Ячейка может быть закрашена (1) или пуста (0)(everyg#(in%(domain01))cells);; Каждой линии ставится в соответсвие свой шаблон(constrain-linesrowshorizontal-hints)(constrain-linescolumnsvertical-hints));; run возвращает множество результатов, берем первый(first);; Возвращаем решение как последовательность рядов(partitiongrid-width))))
Ограничим линии подсказками:
1234567891011
(defn- constrain-lines[lineshints](cond (and (seq lines)(seq hints)); Если остались линии и подсказки(all; Ограничиваем первую линию первой подсказкой и рекурсивно повторяем(match-line-pattern(first lines)(first hints)false)(constrain-lines(rest lines)(rest hints)));; Если закончились линии и подсказки - все прошло хорошо(and (empty?lines)(empty?hints))succeed;; Иначе - что-то пошло не так...:elsefail))
Итак, последнее что нам осталось сделать, это подобрать заполнение для линии соответственно ее шаблону. Для этого определим новое логическое правило match-line-pattern, которое будет перебирать различные варианты окраски клеток кроссворда с учетом уже существующих догадок (полученных от сопоставления других линий пересекающих текущую).
1234567891011121314151617181920212223242526
(defnematch-line-pattern[linepatternchain?];; line - последовательность логических переменных для сопоставления;; pattern - подсказка, накладывающая ограничения на содержимое line;; chain? - флаг, сигнализирующий, что предыдущая ячейка была закрашена([[][]_]); Если линия закончилась и подсказок([[][0]_]); не осталось, то цель выполнена успешно;; Если предыдущая ячейка была закрашена и;; текущая подсказка закончилась (0), значит;; мы дошли до разрыва между закрашенными последовательностями([[0. line-tail][0. pattern-tail]true](match-line-patternline-tailpattern-tailfalse));; Если еще осталась подсказка, то пробуем закрасить ячейку([[1. line-tail][counter. pattern-tail]_](< 0counter)(fresh[new-counternew-pattern](+ new-counter1counter)(consonew-counterpattern-tailnew-pattern)(match-line-patternline-tailnew-patterntrue)));; Если предыдущая ячейка не была закрашена, то;; пробуем оставить текущую пустой([[0. line-tail]_false](match-line-patternline-tailpatternfalse)))