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

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

Возможно, SQL не самый подходящий для этого язык, зато мы рассмотрим его различные возможности, о которых вы могли и не подозревать.


Оригинальная постановка задачи и ее перевод:

Advent of Code 2025, Day 2: Gift Shop

--- День 2: Сувенирный магазин ---

Вы входите внутрь и поднимаетесь на лифте на единственную оставшуюся остановку: сувенирный магазин. «Спасибо за посещение Северного полюса!» — радостно восклицает табличка рядом. Вы не уверены, кому вообще разрешено посещать Северный полюс, но знаете, что отсюда можно попасть в вестибюль, а оттуда — в остальную часть базы на Северном полюсе.

Пока вы пробираетесь сквозь удивительно обширный ассортимент, один из продавцов узнает вас и просит о помощи.

Оказалось, один из юных эльфов играл на компьютере в сувенирном магазине и умудрился добавить в базу данных целую кучу недействительных идентификаторов товаров! Вам же наверняка не составит труда определить недействительные идентификаторы товаров для него, верно?

Они уже проверили большинство диапазонов идентификаторов продуктов; вам нужно проверить лишь несколько из них (ваши данные для головоломки). Например:

11-22,95-115,998-1012,1188511880-1188511890,222220-222224,
1698522-1698528,446443-446449,38593856-38593862,565653-565659,
824824821-824824827,2121212118-2121212124

(Диапазоны идентификаторов здесь перенесены для удобства чтения; в вашем вводе они отображаются на одной длинной строке.)

Диапазоны разделяются запятыми (,); каждый диапазон имеет свой первый идентификатор и последний идентификатор, разделенные тире (-).

Поскольку юный эльф просто выписывал какие-то глупые узоры, вы можете найти недействительные идентификаторы, поискав любой идентификатор, состоящий только из последовательности цифр, повторённых дважды. Таким образом, 55(5дважды), 6464(64дважды) и 123123(123дважды) будут недействительными идентификаторами.

Ни одно из чисел не содержит начальных нулей; 0101это вообще не идентификатор. ( 101— допустимый идентификатор, который следует игнорировать.)

Ваша задача — найти все недействительные идентификаторы в указанных диапазонах. В приведённом выше примере:

  • 11-22имеет два недействительных идентификатора, 11и 22.

  • 95-115имеет один недействительный идентификатор, 99.

  • 998-1012имеет один недействительный идентификатор, 1010.

  • 1188511880-1188511890имеет один недействительный идентификатор, 1188511885.

  • 222220-222224имеет один недействительный идентификатор, 222222.

  • 1698522-1698528не содержит недействительных идентификаторов.

  • 446443-446449имеет один недействительный идентификатор, 446446.

  • 38593856-38593862имеет один недействительный идентификатор, 38593859.

  • Остальные диапазоны не содержат недействительных идентификаторов.

Сложение всех недействительных идентификаторов в этом примере дает 1227775554.

Что получится, если сложить все недействительные удостоверения личности?

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

Клерк быстро обнаруживает, что в диапазонах вашего списка всё ещё есть недействительные идентификаторы. Может быть, юный эльф вытворял и другие глупости?

Итак, идентификатор недействителен, если он состоит только из некоторой последовательности цифр, повторённой как минимум дважды. Таким образом, 12341234(1234два раза), 123123123(123три раза), 1212121212(12пять раз) и 1111111(1семь раз) — всё это недействительные идентификаторы.

Из того же примера, что и раньше:

  • 11-22по-прежнему имеет два недействительных идентификатора, 11и 22.

  • 95-115теперь имеет два недействительных идентификатора, 99и 111.

  • 998-1012теперь имеет два недействительных идентификатора, 999и 1010.

  • 1188511880-1188511890все еще имеет один недействительный идентификатор, 1188511885.

  • 222220-222224все еще имеет один недействительный идентификатор, 222222.

  • 1698522-1698528по-прежнему не содержит недействительных идентификаторов.

  • 446443-446449все еще имеет один недействительный идентификатор, 446446.

  • 38593856-38593862все еще имеет один недействительный идентификатор, 38593859.

  • 565653-565659теперь имеет один недействительный идентификатор, 565656.

  • 824824821-824824827теперь имеет один недействительный идентификатор, 824824824.

  • 2121212118-2121212124теперь имеет один недействительный идентификатор, 2121212121.

Сложение всех недействительных идентификаторов в этом примере дает 4174379265.

Что получится, если сложить все недействительные удостоверения личности, используя эти новые правила?

Часть 1

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

Мы так делать не будем, а воспользуемся тем фактом, что "недействительный идентификатор состоит из последовательности цифр, повторенной дважды" означает простую вещь - это число делится на 10...01 (назовем это число множителем).

Действительно, достаточно вспомнить умножение "столбиком" из начальной школы, чтобы понять, почему:

  • aa = 11 * a

  • abab = 101 * ab

  • abcabc = 1001 * abc

  • ...

Сначала разберем ввод уже знакомой по вчерашней задаче функцией regexp_matches, причем наличие или отсутствие переводов строк нас тут совсем не интересует:

  regexp_matches($$
11-22,95-115,998-1012,1188511880-1188511890,222220-222224,
1698522-1698528,446443-446449,38593856-38593862,565653-565659,
824824821-824824827,2121212118-2121212124
$$, '(\d+)-(\d+)', 'g') ids

Затем определим, сколько нулей может быть в множителе: не менее округленной вверх половины длины первого идентификатора, но не более округленной вниз половины длины второго:

ceil(length(ids[1]) * 0.5) <= zeroes <= floor(length(ids[2]) * 0.5)

Сгенерируем все эти значения множителя функцией generate_series, вычислив сразу и min/max границы диапазона, для которого он подойдет - это все числа соответствующей ему [четной] длины:

10     <= a   * 11   <= 99
1000   <= ab  * 101  <= 9999
100000 <= abc * 1001 <= 999999
...

При этом мы воспользуемся удобной возможностью PostgreSQL легко превращать строковые значения в числовые.

Можно было бы сразу оперировать на уровне чисел со степенями 10, но - зачем?..

Остался последний шаг - непосредственно сгенерировать искомые идентификаторы с шагом множителя в каждом из диапазонов и посчитать их сумму.

Опять же, сумму чисел в интервале, кратных конкретному множителю, можно представить как сумму арифметической прогрессии:

abcabc + abdabd + abeabe + ...= 1001 * (abc + abd + abe + ...) = 1001 * ((abc + 0) + (abc + 1) + (abc + 2) + ...) = 1001 * (abc * N + sum(0, N - 1))

Можно, да, но современных мощностей хватает, чтобы обойтись и без этого.

Проверим себя, сгенерировав все подходящие под условия числа, воспользовавшись возможностью указывать "шаг" для generate_series:

SELECT
  *
--  sum(num)
FROM
  regexp_matches($$
11-22,95-115,998-1012,1188511880-1188511890,222220-222224,
1698522-1698528,446443-446449,38593856-38593862,565653-565659,
824824821-824824827,2121212118-2121212124
$$, '(\d+)-(\d+)', 'g') ids
, LATERAL (
    SELECT
      ('1' || repeat('0', zeroes::integer - 1) || '1')::bigint mul -- множитель
    , ('1' || repeat('0', zeroes::integer * 2 - 1))::bigint min    -- 10..0
    , (repeat('9', zeroes::integer * 2))::bigint max               -- 9..99
    FROM
      generate_series(
        ceil(length(ids[1]) * 0.5)
      , floor(length(ids[2]) * 0.5)
      ) zeroes -- сколько нулей может быть в множителе
  ) T
, generate_series(
    ceil(
      greatest(ids[1]::double precision, min) -- граница интервала справа
    / mul)::bigint * mul                      -- первое кратное на интервале
  , least(ids[2]::bigint, max)                -- граница интервала справа
  , mul                                       -- шаг по множителю
  ) num;

Если все сделано правильно, для исходного примера мы получим вот такую выборку:

 ids                    |  mul   |  min       |  max       |  num
{11,22}                 |     11 |         10 |         99 |         11
{11,22}                 |     11 |         10 |         99 |         22
{95,115}                |     11 |         10 |         99 |         99
{998,1012}              |    101 |       1000 |       9999 |       1010
{1188511880,1188511890} | 100001 | 1000000000 | 9999999999 | 1188511885
{222220,222224}         |   1001 |     100000 |     999999 |     222222
{446443,446449}         |   1001 |     100000 |     999999 |     446446
{38593856,38593862}     |  10001 |   10000000 |   99999999 |   38593859

Для решения задачи остается лишь заменить SELECT * на SELECT sum(num).

Часть 2

Во второй части уже требуется найти числа делящиеся не только на совпадающие "половинки", но и на одинаковые трети, четверти, ..., поэтому код выбора множителя и границ диапазона придется немного модифицировать:

SELECT
  ('1' || repeat('0', digits - 1))::bigint min
, (repeat('9', digits))::bigint max
, mul
FROM
  generate_series(
    length(ids[1])
  , length(ids[2])
  ) digits -- сколько цифр может быть в идентификаторе
, LATERAL (
    SELECT
      (repeat(repeat('0', len - 1) || '1', digits / len))::bigint mul
    FROM
      generate_series(1, digits - 1) len
    WHERE
      digits % len = 0 -- количество цифр должно делиться нацело на группы по len
  ) T

Тут мы удачно пользуемся тем фактом, что преобразование строки в число само отбрасывает лидирующие нули: '001001'::bigint -> 1001.

Проверим, что там сгенерировалось:

SELECT
  *
--  sum(DISTINCT num)
FROM
  regexp_matches($$
11-22,95-115,998-1012,1188511880-1188511890,222220-222224,
1698522-1698528,446443-446449,38593856-38593862,565653-565659,
824824821-824824827,2121212118-2121212124
$$, '(\d+)-(\d+)', 'g') ids
, LATERAL (
    SELECT
      mul
    , ('1' || repeat('0', digits - 1))::bigint min
    , (repeat('9', digits))::bigint max
    FROM
      generate_series(
        length(ids[1])
      , length(ids[2])
      ) digits
    , LATERAL (
        SELECT
          (repeat(repeat('0', len - 1) || '1', digits / len))::bigint mul
        FROM
          generate_series(1, digits - 1) len
        WHERE
          digits % len = 0
      ) T
  ) T
, generate_series(
    ceil(greatest(ids[1]::double precision, min) / mul)::bigint * mul
  , least(ids[2]::bigint, max)
  , mul) num;
 ids                    |  mul      |  min       |  max       |  num
{11,22}                 |        11 |         10 |         99 |         11
{11,22}                 |        11 |         10 |         99 |         22
{95,115}                |        11 |         10 |         99 |         99
{95,115}                |       111 |        100 |        999 |        111
{998,1012}              |       111 |        100 |        999 |        999
{998,1012}              |       101 |       1000 |       9999 |       1010
{1188511880,1188511890} |    100001 | 1000000000 | 9999999999 | 1188511885
{222220,222224}         |    111111 |     100000 |     999999 |     222222
{222220,222224}         |     10101 |     100000 |     999999 |     222222
{222220,222224}         |      1001 |     100000 |     999999 |     222222
{446443,446449}         |      1001 |     100000 |     999999 |     446446
{38593856,38593862}     |     10001 |   10000000 |   99999999 |   38593859
{565653,565659}         |     10101 |     100000 |     999999 |     565656
{824824821,824824827}   |   1001001 |  100000000 |  999999999 |  824824824
{2121212118,2121212124} | 101010101 | 1000000000 | 9999999999 | 2121212121

Обратите внимание, что число 222222 попало в нашу выборку аж 3 раза - 6 x '2', 3 x '22', 2 x '222'. Поэтому при суммировании надо не забыть исключить дубли с помощью sum(DISTINCT num).

На сегодня - все!

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