Осторожно, спойлеры! Не читайте, пока хотите решить задачу самостоятельно.

В этой челлендж-серии статей, начатой с прошлогоднего эвента, попробуем использовать 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
*   +   *   +  

Числа в каждой задаче расположены вертикально; внизу задачи указан символ операции, которую необходимо выполнить. Задачи разделены сплошным столбцом, состоящим только из пробелов. Выравнивание чисел по левому и правому краю внутри каждой задачи можно игнорировать.

Таким образом, этот рабочий лист содержит четыре задачи:

  • 123456=33210

  • 3286498=490

  • 51387215=4243455

  • 6423314=401

Для проверки работы головоногим ученикам дается общая сумма ответов на отдельные задачи. В этом задании общая сумма равна 3321049042434554014277556.

Конечно, сам рабочий лист гораздо шире. Вам нужно будет полностью развернуть его, чтобы четко прочитать задания.

Решите задачи из математического задания. Какова будет общая сумма, полученная путем сложения ответов на все отдельные задачи?

--- Часть вторая ---

Взрослые головоногие моллюски возвращаются, чтобы проверить, как идут дела. Когда они видят, что ваша общая сумма не совпадает с ожидаемой в таблице, они понимают, что забыли объяснить, как читать математические вычисления головоногих.

Математические вычисления для головоногих моллюсков записываются справа налево в столбцах. Каждое число указывается в отдельном столбце, при этом старшая цифра находится вверху, а младшая — внизу. (Задачи по-прежнему разделяются столбцом, состоящим только из пробелов, а символ внизу задачи по-прежнему обозначает используемый оператор.)

Вот ещё раз пример таблицы:

123 328  51 64 
 45 64  387 23 
  6 98  215 314
*   +   *   +  

Если читать задачи справа налево по одному столбцу, то задачи станут совершенно другими:

  • Самая правая задача — 4431623=1058

  • Вторая проблема справа — 17558132=3253600

  • Третья задача справа — 8248369=625

  • Наконец, самая левая проблема — это 356241=8544

Теперь общая сумма составляет 1058325360062585443263827.

Перерешите задачи из математического задания. Какова будет общая сумма, полученная путем сложения ответов на все отдельные задачи?

Часть 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;