Некоторые головоломки можно решать на SQL just for fun, а часть получается выразить на этом декларативном языке даже эффективнее других, императивных.
Попробовать сделать более наглядное решение, а заодно познакомить с некоторыми нетривиальными возможностями PostgreSQL меня натолкнул пост о решении на Python задачи Black and White.
Описание из исходного поста
Black and White — одна из интересных головоломок игры Puzzle Hunt Мельбурнского Университета 2010 года. По сюжету игры вы преследуете загадочного участника ТВ‑шоу в надежде раскрыть его личность. Вам удается пробраться сначала на студию, а затем и в его гримерку.
«Разложите каждую из диаграмм ниже на полоски размером 1×1, 1×2 или 1×3 таким образом, чтобы ни одна сетка не содержала полосок с одинаковым черно‑белым паттерном, включая повороты».
Головоломка представляет собой 25 квадратных полей, расположенных в 5 рядов и 5 столбцов. В свою очередь, каждое поле разделено на 25 клеток горизонтальными и вертикальными линиями. Клетки имеют белый или черный фон, и каждая из них содержит по одной букве.
Воспользуемся рассуждениями из оригинала
Сначала определим, какие полоски мы можем использовать для решения этой задачи. Существует 6 различных полосок размером 1×3 (WWW, WWB, WBW, WBB, BWB и BBB), 3 различных полоски размером 1×2 (WW, WB и BB) и 2 различных полоски размером 1×1 (W и B). В сумме все эти полоски содержат 26 клеток. Это означает, что для разложения каждого поля придется использовать все полоски размером 1×3, все полоски размером 1×2 и только одну из полосок размером 1×1. Так как расположение полоски из 1 клетки вытекает из расположения остальных 9 полосок, то ее можно не учитывать при разложении. Стало быть, задачу можно переформулировать следующим образом: необходимо расположить на каждом поле 6 уникальных полосок размером 1×3 и 3 уникальных полоски размером 1×2 таким образом, чтобы цвета клеток поля и цвета клеток полосок совпадали.
Начнем «собирать» CTE для запроса-решателя с перечисления всех допустимых полосок:
-- перечисляем возможные по условию полоски , blks AS ( SELECT * FROM unnest(ARRAY['www', 'wwb', 'wbw', 'wbb', 'bwb', 'bbb', 'ww', 'wb', 'bb']) WITH ORDINALITY T(blk, i) -- автонумерация строк )
Мы воспользовались функцией unnest для перевода массива в список строк с их автонумерацией с помощью WITH ORDINALITY
:
blk | i text | bigint www | 1 wwb | 2 wbw | 3 wbb | 4 bwb | 5 bbb | 6 ww | 7 wb | 8 bb | 9
Я целенаправленно вынес вперед более длинные «полоски» по 3 клетки, чтобы уменьшить объем перебора, — ведь их размещений их на поле заведомо меньше, чем у коротких.
Матрица поля
-- переводим "матрицу" в координаты , grid AS ( SELECT x ,y ,c[1] FROM ( VALUES(' bwwbb bwwbw wbwbw bwbbb bwbww ') -- исходная "матрица" поля ) T(src) ,regexp_matches(src, '[bw]+', 'g') WITH ORDINALITY y(line, y) -- выделяем и нумеруем строки ,regexp_matches(line[1], '[bw]', 'g') WITH ORDINALITY x(c, x) -- нумеруем клетки и получаем их значения )
Регулярными выражениями (функция regexp_matches
) мы получаем из текстового представления поля значения в каждой координатной клетке:
x | y | c bigint | bigint | text 1 | 1 | b 2 | 1 | w 3 | 1 | w 4 | 1 | b 5 | 1 | b 1 | 2 | b 2 | 2 | w 3 | 2 | w 4 | 2 | b ...
Строки «по столбцам»
Чтобы удобнее определять наложение полосок на поле не только «по горизонтали», но и «по вертикали», сформируем текстовое представление для каждого из столбцов (ну и для строк заодно):
-- формируем массивы строк/столбцов , grid_both AS ( SELECT array_agg(line ORDER BY x) FILTER(WHERE y IS NOT NULL) gridh ,array_agg(line ORDER BY y) FILTER(WHERE x IS NOT NULL) gridv FROM ( SELECT x ,y ,string_agg(c, '' ORDER BY x, y) line FROM grid GROUP BY GROUPING SETS (x, y) ) T )
С помощью наборов группирования мы «сложили» строки сразу в обеих проекциях:
x | y | line bigint | bigint | text 1 | | bbwbb 2 | | wwbww 3 | | wwwbb 4 | | bbbbw 5 | | bwwbw | 1 | bwwbb | 2 | bwwbw | 3 | wbwbw | 4 | bwbbb | 5 | bwbww
… а затем превратили их в два массива, разложив на строки сразу и оригинальную «матрицу» и транспонированную:
gridh | gridv text[] | text[] {bwwbb,bwwbw,wbwbw,bwbbb,bwbww} | {bbwbb,wwbww,wwwbb,bbbbw,bwwbw}
Возможные размещения
Вместо попыток «положить» плитку на подходящие по узору клетки поля мы будем решать обратную задачу — для каждой клетки сформируем все возможные горизонтальные/вертикальные полоски длин 2 и 3
-- формируем для каждой координаты все варианты горизонтальных (вправо) и вертикальных (вниз) полосок длин 2 и 3 , placement AS ( SELECT x ,y ,ln ,d ,blk FROM grid_both ,generate_subscripts(gridh, 1) y -- перебираем индексы массивов ,generate_subscripts(gridv, 1) x ,unnest(ARRAY[2, 3]) ln ,LATERAL ( VALUES ('h', substr(gridh[y], x, ln)) -- формируем горизонтальную ... ,('v', substr(gridv[x], y, ln)) -- ... и вертикальную полоски ) T(d, blk) WHERE length(blk) = ln -- отсекаем "вылезшие" за границы поля )
Поскольку «полоску» мы нарезали функцией substr
, то при выходе за границы поля, ее длина окажется меньше желаемой, и лишнее сразу отсекается:
x | y | ln | d | blk 1 | 1 | 2 | h | bw 1 | 1 | 3 | h | bww 1 | 1 | 2 | v | bb 1 | 1 | 3 | v | bbw 1 | 2 | 2 | h | bw 1 | 2 | 3 | h | bww 1 | 2 | 2 | v | bw ...
Рекурсивный перебор возможных размещений
Итак, для каждой полоски в исходном наборе мы можем попробовать «положить» ее в доступное место в «прямом» или «зеркальном» положении. Для подходящего размещения заполним массив занимаемых ячеек и их принадлежность. Применим тот же подход, что и в предыдущем решении: если у массивов уже занятых и занимаемых на этом шаге ячеек нет пересечений — можно разместить полоску, а координаты ее ячеек — добавить:
-- рекурсивно для каждой полоски перебираем варианты ее размещения , r AS ( SELECT 1::bigint i ,'{}'::text[] acc_cell -- массив занятых ячеек ,'{}'::text[] acc_blk -- принадлежность занятых ячеек UNION ALL SELECT i + 1 ,acc_cell || fill_cell ,acc_blk || fill_blk FROM r JOIN blks USING(i) JOIN placement p ON p.blk IN (blks.blk, reverse(blks.blk)) -- прямое и зеркальное размещение , LATERAL ( SELECT array_agg(ARRAY[cx, cy]::text) fill_cell ,array_agg(ARRAY[cx, cy, i]::text) fill_blk FROM -- набор координат ячеек, занимаемых полоской generate_series(0, ln - 1) j ,LATERAL ( SELECT CASE d WHEN 'h' THEN x + j WHEN 'v' THEN x END cx ,CASE d WHEN 'h' THEN y WHEN 'v' THEN y + j END cy ) T ) T WHERE NOT (fill_cell && acc_cell) -- планируемое размещение не имеет пересечений )
Массив занятых ячеек содержит текстовые преставления пар координат [x, y]
, а принадлежность ячейки конкретной полоске определяется триплетом [x, y, i]
:
i | acc_cell | acc_blk bigint | text[] | text[] 1 | {} | {} 2 | {{3,1},{3,2},{3,3}} | {{3,1,1},{3,2,1},{3,3,1}} 3 | {{3,1},{3,2},{3,3},{2,1},{2,2},{2,3}} | {{3,1,1},{3,2,1},{3,3,1},{2,1,2},{2,2,2},{2,3,2}} ...
Как можно заметить, первая полоска www
легла в единственное допустимое место по координатам [3;1]
вертикально вниз, а следующая wwb
— в [2;1]
.
Матрица заполнения
Пойдем на допущение, что искомая комбинация размещения — единственная. Тогда достаточно взять массив принадлежности из строки с наибольшим номером и «развернуть» в строки-ячейки:
-- переводим массив в координаты , grid_fill AS ( SELECT cell[1] x ,cell[2] y ,cell[3] i FROM unnest(( -- разворачиваем финальное размещение SELECT acc_blk FROM r ORDER BY i DESC LIMIT 1 )) ,LATERAL ( SELECT unnest::integer[] cell -- превращаем текстовый массив в целочисленный ) T )
x | y | i integer | integer | integer 3 | 1 | 1 3 | 2 | 1 3 | 3 | 1 2 | 1 | 2 2 | 2 | 2 2 | 3 | 2 2 | 5 | 3 ...
Вывод матрицы
Можно просто определить координаты «отсутствующей» клетки, но мы для самопроверки соберем красивый вывод, демонстрируя какой полоской какая ячейка накрыта, на забыв подменить единственную незаполненную ячейку на '.'
:
SELECT string_agg(coalesce(i::text, '.') || c, ' ' ORDER BY x) FROM grid NATURAL LEFT JOIN grid_fill GROUP BY y ORDER BY y;
string_agg text 4b 2w 1w 9b 9b 4b 2w 1w 6b 7w 4w 2b 1w 6b 7w 5b 5w 5b 6b 8b .b 3w 3b 3w 8w
Итоговый запрос-решатель выглядит ничуть не сложнее кода на Python из оригинального поста.
Полный текст запроса
-- перечисляем возможные по условию полоски WITH RECURSIVE blks AS ( SELECT * FROM unnest(ARRAY['www', 'wwb', 'wbw', 'wbb', 'bwb', 'bbb', 'ww', 'wb', 'bb']) WITH ORDINALITY T(blk, i) -- автонумерация строк ) -- переводим "матрицу" в координаты , grid AS ( SELECT x ,y ,c[1] FROM ( VALUES(' bwwbb bwwbw wbwbw bwbbb bwbww ') -- исходная "матрица" поля ) T(src) ,regexp_matches(src, '[bw]+', 'g') WITH ORDINALITY y(line, y) -- выделяем и нумеруем строки ,regexp_matches(line[1], '[bw]', 'g') WITH ORDINALITY x(c, x) -- нумеруем клетки и получаем их значения ) -- формируем массивы строк/столбцов , grid_both AS ( SELECT array_agg(line ORDER BY x) FILTER(WHERE y IS NOT NULL) gridh ,array_agg(line ORDER BY y) FILTER(WHERE x IS NOT NULL) gridv FROM ( SELECT x ,y ,string_agg(c, '' ORDER BY x, y) line FROM grid GROUP BY GROUPING SETS (x, y) ) T ) -- формируем для каждой координаты все варианты горизонтальных (вправо) и вертикальных (вниз) полосок длин 2 и 3 , placement AS ( SELECT x ,y ,ln ,d ,blk FROM grid_both ,generate_subscripts(gridh, 1) y -- перебираем индексы массивов ,generate_subscripts(gridv, 1) x ,unnest(ARRAY[2, 3]) ln ,LATERAL ( VALUES ('h', substr(gridh[y], x, ln)) -- формируем горизонтальную ... ,('v', substr(gridv[x], y, ln)) -- ... и вертикальную полоски ) T(d, blk) WHERE length(blk) = ln -- отсекаем "вылезшие" за границы поля ) -- рекурсивно для каждой полоски перебираем варианты ее размещения , r AS ( SELECT 1::bigint i ,'{}'::text[] acc_cell -- массив занятых ячеек ,'{}'::text[] acc_blk -- принадлежность занятых ячеек UNION ALL SELECT i + 1 ,acc_cell || fill_cell ,acc_blk || fill_blk FROM r JOIN blks USING(i) JOIN placement p ON p.blk IN (blks.blk, reverse(blks.blk)) -- прямое и зеркальное размещение , LATERAL ( SELECT array_agg(ARRAY[cx, cy]::text) fill_cell ,array_agg(ARRAY[cx, cy, i]::text) fill_blk FROM -- набор координат ячеек, занимаемых полоской generate_series(0, ln - 1) j ,LATERAL ( SELECT CASE d WHEN 'h' THEN x + j WHEN 'v' THEN x END cx ,CASE d WHEN 'h' THEN y WHEN 'v' THEN y + j END cy ) T ) T WHERE NOT (fill_cell && acc_cell) -- планируемое размещение не имеет пересечений ) -- переводим массив в координаты , grid_fill AS ( SELECT cell[1] x ,cell[2] y ,cell[3] i FROM unnest(( -- разворачиваем финальное размещение SELECT acc_blk FROM r ORDER BY i DESC LIMIT 1 )) ,LATERAL ( SELECT unnest::integer[] cell -- превращаем текстовый массив в целочисленный ) T ) SELECT string_agg(coalesce(i::text, '.') || c, ' ' ORDER BY x) FROM grid NATURAL LEFT JOIN grid_fill GROUP BY y ORDER BY y;
ссылка на оригинал статьи https://habr.com/ru/articles/845386/
Добавить комментарий