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

Комментарии (0)