Некоторые головоломки можно решать на 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;