Осторожно, спойлеры! Не читайте, пока хотите решить задачу самостоятельно.
В этой челлендж-серии статей, начатой с прошлогоднего эвента, попробуем использовать PostgreSQL как среду для решения задач Advent of Code 2025.
Возможно, SQL не самый подходящий для этого язык, зато мы рассмотрим его различные возможности, о которых вы могли и не подозревать.

Оригинальная постановка задачи и ее перевод:
Advent of Code 2025, Day 5: Cafeteria
--- День 5: Кафетерий ---
Когда погрузчики пробивают стену, эльфы с радостью обнаруживают, что по ту сторону стены все-таки находится кафетерий.
Из кухни доносится шум. «Такими темпами у нас совсем не останется времени, чтобы повесить венки в столовой!» Непреклонные в своем стремлении, вы отправляетесь на место происшествия.
«Если бы только мы не перешли на новую систему управления запасами прямо перед Рождеством!» — восклицает другой эльф. Вы спрашиваете, что происходит.
Эльфы на кухне объясняют ситуацию: из-за своей сложной новой системы учета они не могут определить, какие ингредиенты свежие, а какие испорченные. Когда вы спрашиваете, как это работает, они дают вам копию своей базы данных (ввод головоломки).
База данных работает с идентификаторами ингредиентов. Она состоит из списка диапазонов идентификаторов свежих ингредиентов, пустой строки и списка доступных идентификаторов ингредиентов. Например:
3-5 10-14 16-20 12-18 1 5 8 11 17 32
Диапазоны идентификаторов свежих ингредиентов являются включающими: диапазон 3-5 означает, что идентификаторы ингредиентов 3, 4, и 5являются свежими. Диапазоны также могут перекрываться; идентификатор ингредиента считается свежим, если он находится в любом из диапазонов.
Эльфы пытаются определить, какие из доступных идентификаторов ингредиентов являются свежими. В этом примере это делается следующим образом:
Ингредиент с идентификатором
1испорчен, поскольку не попадает ни в один из диапазонов.Ингредиент с идентификатором
5считается свежим, поскольку он находится в заданном диапазоне3-5.Ингредиент с идентификатором
8испорчен.Ингредиент с идентификатором
11считается свежим, поскольку он находится в заданном диапазоне10-14.Ингредиент с идентификатором
17считается свежим, поскольку он попадает в диапазон значений16-20, а также в диапазон12-18.Ингредиент с идентификатором
32испорчен.
Таким образом, в этом примере 3 из всех доступных ингредиентов являются свежими.
Обработайте файл базы данных из новой системы управления запасами. Сколько из имеющихся ингредиентов являются свежими?
--- Часть вторая ---
Эльфы начинают выносить испорченные продукты в мусоропровод в задней части кухни.
Чтобы они перестали вас беспокоить, когда у них появляет��я новый товар, эльфы хотели бы знать все идентификаторы ингредиентов, которые, согласно диапазонам идентификаторов свежих ингредиентов, считаются свежими. Идентификатор ингредиента по-прежнему считается свежим, если он находится в любом из этих диапазонов.
Теперь второй раздел базы данных (доступные идентификаторы ингредиентов) не имеет значения. Вот диапазоны идентификаторов ингредиентов из приведенного выше примера:
3-5 10-14 16-20 12-18
Идентификаторы ингредиентов в этих диапазонах считаются свежими, это 3, 4, 5, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19и 20. Таким образом, в этом примере диапазоны идентификаторов свежих ингредиентов содержат в общей сложности 14 штук.
Повторно обработайте файл базы данных. Сколько всего ингредиентов считаются свежими в соответствии с диапазонами идентификаторов свежих ингредиентов?
Часть 1
У нас не так уж много что диапазонов, что идентификаторов, поэтому мы можем позволить себе банальный квадратичный алгоритм проверки каждого идентификатора в каждом из диапазонов.
Кроме того, значения идентификаторов хоть и велики, но не слишком, чтобы не уместиться в тип bigint - им и воспользуемся:
выделим с помощью
regexp_matchesвсе имеющиеся диапазоны и идентификаторыа потом просто подсчитаем количество идентификаторов, для которых есть хотя бы один подходящий диапазон
WITH src AS ( SELECT $$ 3-5 10-14 16-20 12-18 1 5 8 11 17 32 $$ ) , rngs AS ( SELECT ids[1]::bigint src -- определяем диапазоны , ids[2]::bigint dst FROM regexp_matches((TABLE src), '(\d+)-(\d+)', 'g') ids ) , ids AS ( SELECT id[1]::bigint -- определяем идентификаторы FROM regexp_matches((TABLE src), '[\r\n](\d+)(?=[\r\n]|$)', 'g') id ) SELECT count(*) FROM ids WHERE EXISTS( -- есть хотя бы один подходящий диапазон SELECT NULL FROM rngs WHERE id BETWEEN src AND dst -- идентификатор в диапазоне LIMIT 1 );
Часть 2
Решение более сложной задачи на определение суммарной длины всех диапазонов с учетом возможных пересечений с использованием возможностей PostgreSQL оказывается даже короче решения первой части:
преобразуем все входные диапазоны в тип int8range, включающие ��вои границы
воспользуемся функцией range_agg, чтобы получить мультидиапазон, представляющий объединение диапазонов, входящих в него
"разворачиваем" мультидиапазон в "обычные" диапазоны функцией
unnestосталось только подсчитать сумму длин (разность значений верхней и нижней границ,
upper - lower) каждого диапазона
SELECT sum(upper(rng) - lower(rng)) -- сумма длин FROM ( SELECT unnest(range_agg(rng)) rng -- "разворачиваем" мультидиапазон-объединение FROM ( SELECT int8range( -- диапазон из bigint-значений ids[1]::bigint , ids[2]::bigint , '[]' -- включение границ диапазона ) rng FROM regexp_matches($$ 3-5 10-14 16-20 12-18 $$, '(\d+)-(\d+)', 'g') ids ) rngs ) T;
Если посмотреть на исходном примере, то пошагово это будет выглядеть так:
rng int8range [3,6) -- диапазон [3,5] в целых числах равен [3,6), без включения правой границы [10,15) [16,21) [12,19)
После "разворачивания" объединенного диапазона получим:
rng int8range [3,6) -- длина 3 [10,21) -- длина 11
