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

Оригинальная постановка задачи и ее перевод:
Advent of Code 2025, Day 6: Trash Compactor
--- День 6: Мусороуборочная машина ---
После того, как вы помогли эльфам на кухне, вы отдыхали и помогали им разыгрывать сцену из фильма, когда с чрезмерным энтузиазмом прыгнули в мусоропровод!
После непродолжительного падения вы оказываетесь внутри мусородробилки. К сожалению, дверь запечатана магнитом.
Пока вы пытаетесь найти выход, к вам подходит семейство головоногих моллюсков! Они почти уверен��, что смогут открыть дверь, но это займет некоторое время. Пока вы ждете, они интересуются, не могли бы вы помочь самой младшей головоноге с домашним заданием по математике.
Математика головоногих внешне не сильно отличается от обычной математики. Математический рабочий лист (ваш ввод данных для головоломки) состоит из списка задач; каждая задача содержит группу чисел, которые необходимо либо сложить (+), либо умножить (*).
Однако задачи расположены несколько странно; они, кажется, представлены рядом друг с другом в очень длинном горизонтальном списке. Например:
123 328 51 64
45 64 387 23
6 98 215 314
* + * + Числа в каждой задаче расположены вертикально; внизу задачи указан символ операции, которую необходимо выполнить. Задачи разделены сплошным столбцом, состоящим только из пробелов. Выравнивание чисел по левому и правому краю внутри каждой задачи можно игнорировать.
Таким образом, этот рабочий лист содержит четыре задачи:
123*45*6=33210328+64+98=49051*387*215=424345564+23+314=401
Для проверки работы головоногим ученикам дается общая сумма ответов на отдельные задачи. В этом задании общая сумма равна 33210+ 490+ 4243455+ 401= 4277556.
Конечно, сам рабочий лист гораздо шире. Вам нужно будет полностью развернуть его, чтобы четко прочитать задания.
Решите задачи из математического задания. Какова будет общая сумма, полученная путем сложения ответов на все отдельные задачи?
--- Часть вторая ---
Взрослые головоногие моллюски возвращаются, чтобы проверить, как идут дела. Когда они видят, что ваша общая сумма не совпадает с ожидаемой в таблице, они понимают, что забыли объяснить, как читать математические вычисления головоногих.
Математические вычисления для головоногих моллюсков записываются справа налево в столбцах. Каждое число указывается в отдельном столбце, при этом старшая цифра находится вверху, а младшая — внизу. (Задачи по-прежнему разделяются столбцом, состоящим только из пробелов, а символ внизу задачи по-прежнему обозначает используемый оператор.)
Вот ещё раз пример таблицы:
123 328 51 64
45 64 387 23
6 98 215 314
* + * + Если читать задачи справа налево по одному столбцу, то задачи станут совершенно другими:
Самая правая задача —
4+431+623=1058Вторая проблема справа —
175*581*32=3253600Третья задача справа —
8+248+369=625Наконец, самая левая проблема — это
356*24*1=8544
Теперь общая сумма составляет 1058+ 3253600+ 625+ 8544= 3263827.
Перерешите задачи из математического задания. Какова будет общая сумма, полученная путем сложения ответов на все отдельные задачи?
Часть 1
Решим предлагаемые задачи "в три действия".
Во-первых, распознаем и пронумеруем те элементы (числа и знаки операций), которые у нас есть на листе. Параллельно определим общее количество столбцов - оно равно общему количеству знаков операций:
WITH elem AS (
SELECT
i - 1 i -- номер элемента, начиная с 0
, elem[1] -- элемент задачи - число или знак операции
, count(*) FILTER(WHERE elem[1] IN ('+', '*')) OVER() cols -- общее кол-во столбцов
FROM
regexp_matches($$
123 328 51 64
45 64 387 23
6 98 215 314
* + * +
$$, '(\d+|\+|\*)', 'g')
WITH ORDINALITY T(elem, i)
) i | elem | cols
0 | 123 | 4
1 | 328 | 4
2 | 51 | 4
3 | 64 | 4
4 | 45 | 4
5 | 64 | 4
6 | 387 | 4
7 | 23 | 4
8 | 6 | 4
9 | 98 | 4
10 | 215 | 4
11 | 314 | 4
12 | * | 4
13 | + | 4
14 | * | 4
15 | + | 4Заметим, что все элементы, относящиеся к одной задаче, имеют один и тот же остаток от деления на количество столбцов - сгруппируем их, сразу вычисляя агрегат, соответствующий операции:
SELECT
CASE min(elem) FILTER(WHERE elem IN ('+', '*')) -- оператор
WHEN '+' THEN
sum(elem::bigint) FILTER(WHERE elem NOT IN ('+', '*')) -- сумма чисел
WHEN '*' THEN
exp(sum(ln(elem::bigint)) FILTER(WHERE elem NOT IN ('+', '*')))::bigint -- произведение чисел
END res
FROM
elem
GROUP BY
i % colsЗдесь мы использовали агрегатную функцию min для определения "первого и единственного" в столбце подходящего под FILTER-условия оператора.
А для вычисления произведения набора чисел, агрегатной функции для которого нет, выразим его через сумму логарифмов:

Осталось лишь просуммировать все полученные по задачам ответы:
WITH elem AS (
SELECT
i - 1 i -- номер элемента, начиная с 0
, elem[1] -- элемент задачи - число или знак операции
, count(*) FILTER(WHERE elem[1] IN ('+', '*')) OVER() cols -- общее кол-во столбцов
FROM
regexp_matches($$
123 328 51 64
45 64 387 23
6 98 215 314
* + * +
$$, '(\d+|\+|\*)', 'g')
WITH ORDINALITY T(elem, i)
)
SELECT
sum(res)
FROM
(
SELECT
CASE min(elem) FILTER(WHERE elem IN ('+', '*')) -- оператор
WHEN '+' THEN
sum(elem::bigint) FILTER(WHERE elem NOT IN ('+', '*')) -- сумма чисел
WHEN '*' THEN
exp(sum(ln(elem::bigint)) FILTER(WHERE elem NOT IN ('+', '*')))::bigint -- произведение чисел
END res
FROM
elem
GROUP BY
i % cols
) T;Часть 2
Во второй части нас просят читать числа с листа не по строкам, а по столбцам.
"Пересоберем" числа, сначала разбив текст на строки, строки - на символы, а символы с помощью string_agg скомпонуем уже по столбцам:
WITH elem AS(
SELECT
col -- номер столбца
, string_agg(c, '' ORDER BY row) FILTER(WHERE c ~ '\d')::bigint num -- это число
, min(c) FILTER(WHERE c IN ('+', '*')) op -- это оператор
FROM
regexp_split_to_table($$
123 328 51 64
45 64 387 23
6 98 215 314
* + * +
$$, '[\r\n]+')
WITH ORDINALITY line(line, row) -- разбиение по строками
, regexp_split_to_table(line, '')
WITH ORDINALITY char(c, col) -- разбиение по символам (столбцам)
WHERE
line <> '' -- сразу пропускаем пустые строки
GROUP BY
col
)col | num | op
1 | 1 | *
2 | 24 |
3 | 356 |
4 | |
5 | 369 | +
6 | 248 |
7 | 8 |
8 | |
9 | 32 | *
10 | 581 |
11 | 175 |
12 | |
13 | 623 | +
14 | 431 |
15 | 4 |Теперь нам необходимо определить, к какой из задач относятся конкретные операторы и числа - для этого пронумеруем их по количеству встретившихся нам выше по выборке операторов с помощью оконной функции count:
, task AS (
SELECT
count(op <> '') OVER(ORDER BY col) task -- номер задачи - количество операторов перед строкой
, num
, op
FROM
elem
WHERE
num IS NOT NULL -- пропускаем пустые строки
)task | num | op
1 | 1 | *
1 | 24 |
1 | 356 |
2 | 369 | +
2 | 248 |
2 | 8 |
3 | 32 | *
3 | 581 |
3 | 175 |
4 | 623 | +
4 | 431 |
4 | 4 |Ну а дальше - задача сведена к предыдущей! Разве что преобразовывать строки в числа и вычислять номер задачи нам больше не требуется:
WITH elem AS(
SELECT
col -- номер столбца
, string_agg(c, '' ORDER BY row) FILTER(WHERE c ~ '\d')::bigint num -- это число
, min(c) FILTER(WHERE c IN ('+', '*')) op -- это оператор
FROM
regexp_split_to_table($$
123 328 51 64
45 64 387 23
6 98 215 314
* + * +
$$, '[\r\n]+')
WITH ORDINALITY line(line, row) -- разбиение по строками
, regexp_split_to_table(line, '')
WITH ORDINALITY char(c, col) -- разбиение по символам (столбцам)
WHERE
line <> '' -- сразу пропускаем пустые строки
GROUP BY
col
)
, task AS (
SELECT
count(op <> '') OVER(ORDER BY col) task -- номер задачи - количество операторов перед строкой
, num
, op
FROM
elem
WHERE
num IS NOT NULL -- пропускаем пустые строки
)
SELECT
sum(res)
FROM
(
SELECT
CASE min(op) FILTER(WHERE op IN ('+', '*')) -- оператор
WHEN '+' THEN
sum(num) -- сумма чисел
WHEN '*' THEN
exp(sum(ln(num)))::bigint -- произведение чисел
END res
FROM
task
GROUP BY
task
) T;