Решение японских кроссвордов одним запросом SQL

от автора

Привет хабр! Приближается день программиста, и я спешу поделиться своими ненормальными наработками.
Японский кроссворд — NP-полная задача, как и задача коммивояжёра, укладки рюкзака и др. Когда ее решает человек, следует последовательно определять гарантированно заполненные и пустые ячейки. Одну за другой вычеркивать колонки и строки, пока не сложится весь рисунок. Как же возможно запрограммировать решение подобной задачи на языке, который официально даже не является языком программирования, не содержит циклов и переменных? SQL — язык запросов, его главная задача — выбирать строки. Вот мы и будем генерировать множество всех возможных перестановок и, словно скульптор, отсекать все лишнее.

Для воспроизведения запроса потребуется Oracle Database 11g или выше (12-ый на подходе). Более ранние версии не подойдут из-за применения аналитической функции listagg. Возможно использование Express Edition (XE), но этот огрызок use up to 1GB of memory, так что максимальный размер поля, которое может перелопатить эта версия — 5×4. Уже на поле 5×5 лимит памяти заканчивается.

Запрос не использует никаких реальных таблиц базы данных, готовить для него ничего не надо. Входные данные передаются в подзапросе WITH. Можно использовать предложенный кроссворд или составить свой. Для этого следует описать задание – список чисел над и слева от кроссворда, указать размер поля. Также можно по своему вкусу указать символы для заполненных и пустых ячеек.

В процессе работы строк перебрать придется много, нереально много! Поле состоит из ячеек, общим числом кол-во строк * кол-во колонок. Каждая ячейка может быть заполненной (1) или пустой (0). Тогда количество всех возможных перестановок растет нелинейно по формуле 2^(кол-во ячеек). А так как размер поля заранее не фиксирован, каждая ячейка представляется отдельной строкой. В итоге полученное число перестановок нужно умножить еще на количество ячеек. Вот, например, несколько квадратных полей:
3×3 = 4 608
4×4 = 1 048 576
5×5 = 838 860 800
6×6 = 412 316 860 416
8×8 = 1 180 591 620 717 411 303 424

Положительная сторона полного перебора – найдутся все решения. Так что, если у вас простаивают вычислительные мощности, теперь вы знаете чем их занять! Про алгоритм не рассказываю – читать нужно с конца, каждый подзапрос прокомментирован.

Сам запрос

with INPUT as (   ----------------------------- Входные данные ---------------------------------   -- Укажите список чисел задания для колонок и строк. Каждую колонку или строку   -- разделяйте запятой. Группы (не более 5) внутри - пробелом.   -- Если необходимо, скорректируйте размер игрового поля.   -- Опционально можно указать символы-заполнители.   select        '2, 1 1, 1 2, 3' as FIELD_COLS, -- значения колонок слева направо       '2, 1 1, 1 2, 3' as FIELD_ROWS, -- значения строк сверху вниз       4 as COL_COUNT, -- количество колонок игрового поля       4 as ROW_COUNT, -- количество строк игрового поля       '#' as FILL, -- символ-заполнитель заполненной ячейки       ' ' as EMPT  -- символ-заполнитель пустой ячейки   from dual )  -------------------------------------------------------------------------------- -- Вывод: для каждой перестановки выбирается только одна строка с ответом. select    max(RES) as SOLUTION from (   -- Склейка строки с ответом (каждая ячейка перестановки содержит одно и то же)   select      PERM,     listagg(       decode(VAL, '1', FILL, EMPT) ||        decode(mod(CELL, COL_COUNT), 0, chr(13))     ) within group (order by PERM, CELL) over (partition by PERM) as RES   from   (     -- Фильтрация только возможных перестановок.     select        CELL, PERM, VAL     from     (       -- По значениям строки или колонки создается правило, например        -- '1011001110' -> '1 2 3', полученное правило сверяется с исходным.        -- Если правила по колонке и строке ячейки совпали, ячейка помечается        -- как возможная 1, иначе 0. Если в перестановке есть хоть одна        -- невозможная ячейка, все ячейки перестановки помечаются невозможными.       select          CELL, PERM, VAL,         min(           case when              nvl(trim(replace(               length(regexp_substr(VALUES_COL, REG, 1, 1, 'c', 1)) || ' ' ||               length(regexp_substr(VALUES_COL, REG, 1, 1, 'c', 2)) || ' ' ||               length(regexp_substr(VALUES_COL, REG, 1, 1, 'c', 3)) || ' ' ||               length(regexp_substr(VALUES_COL, REG, 1, 1, 'c', 4)) || ' ' ||               length(regexp_substr(VALUES_COL, REG, 1, 1, 'c', 5))               ,' 0', ''             )), '0') = RULE_COL             and             nvl(trim(replace(               length(regexp_substr(VALUES_ROW, REG, 1, 1, 'c', 1)) || ' ' ||               length(regexp_substr(VALUES_ROW, REG, 1, 1, 'c', 2)) || ' ' ||               length(regexp_substr(VALUES_ROW, REG, 1, 1, 'c', 3)) || ' ' ||               length(regexp_substr(VALUES_ROW, REG, 1, 1, 'c', 4)) || ' ' ||               length(regexp_substr(VALUES_ROW, REG, 1, 1, 'c', 5))               ,' 0', ''             )), '0') = RULE_ROW              then 1             else 0           end         ) over (partition by PERM) as IS_PERM_VALID       from       (         -- Получение значений всех ячеек, составляющих строку и колонку          -- для каждой перестановки по оси X и Y, например '1011001110'.         select            CELL, RULE_COL, RULE_ROW, PERM, VAL,           listagg(VAL)              within group (order by PERM, X, CELL)              over (partition by PERM, X) as VALUES_COL,           listagg(VAL)              within group (order by PERM, Y, CELL)              over (partition by PERM, Y) as VALUES_ROW,           '0*(1*)0*(1*)0*(1*)0*(1*)0*(1*)0*' as REG         from         (           -- Строки ячеек умножаются на строки перестановок.           -- Генерация значений (1/0) каждой ячейки для каждой перестановки.           select              CELL, X, Y, RULE_COL, RULE_ROW, PERM,             to_char(mod(trunc(PERM / power(2, CELL -1)), 2)) as VAL           from           (             -- Каждой ячейке выбирается соответствующее ей правило по осям X и Y             -- путем извлечения подстроки динамически строящимся рег. выражением             select                CELL, X, Y,               trim(regexp_substr(                 FIELD_COLS,                  substr(rpad('s', X * length(REG) +1, REG), 3),                  1, 1, 'c', X               )) as RULE_COL,               trim(regexp_substr(                 FIELD_ROWS,                  substr(rpad('s', Y * length(REG) +1, REG), 3),                  1, 1, 'c', Y               )) as RULE_ROW             from              (               -- Точка входа здесь!               -- Получение строк в количестве ячеек игрового поля,                -- рассчет координат ячеек по осям X и Y.                select                  LEVEL as CELL, -- номер поля                 mod(LEVEL -1, COL_COUNT) +1 as X,                 trunc((LEVEL -1) / COL_COUNT) +1 as Y,                 ',([^,]+)' as REG,                 FIELD_COLS, FIELD_ROWS               from INPUT               connect by LEVEL <= COL_COUNT * ROW_COUNT             )           ),           (             -- Генерация строк всех возможных перестановок             select                LEVEL -1 as PERM -- номер перестановки, начиная с 0             from INPUT             connect by LEVEL <= power(2, COL_COUNT * ROW_COUNT)           )         )       )     )     where IS_PERM_VALID = 1   ),   (     -- Получение настроек для визуализации ответа      select COL_COUNT, FILL, EMPT from INPUT   ) ) group by PERM; 

ссылка на оригинал статьи http://habrahabr.ru/post/192752/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *