В этой челлендж-серии статей попробуем использовать PostgreSQL как среду для решения задач Advent of Code 2024.
Возможно, SQL не самый подходящий для этого язык, зато мы рассмотрим его различные возможности, о которых вы могли и не подозревать.
В этой части мы научимся вычислять состояния клеточного автомата без прямого моделирования и узнаем, как можно среди них найти интересное.
Решение Day 1: Historian Hysteria (регулярные выражения и условная агрегация)
Решение Day 5: Print Queue (поиск в словаре и массивах, сортировка "пузырьком")
Решение Day 6: Guard Gallivant (рекурсивные циклы и их контроль)
Решение Day 8: Resonant Collinearity (генерация и подсчет уникальных комбинаций)
Решение Day 11: Plutonian Pebbles (агрегация внутри рекурсии)
Решение Day 12: Garden Groups (волновой алгоритм и подсчет границ)
Решение Day 14: Restroom Redoubt (находим "елочку" с помощью центра масс)
Решение Day 15: Warehouse Woes (играем в сокобан с помощью json-карты и типа point)
Решение Day 16: Reindeer Maze (укрощаем рекурсию в лабиринте)
Решение Day 17: Chronospatial Computer (подбираем значение ветвлением)
Решение Day 19: Linen Layout (динамическое программирование)
Решение Day 20: Race Condition (кратчайший путь "туда и обратно" и его самосоединение)
Решение Day 21: Keypad Conundrum (моделирование против подсчета)

Оригинальная постановка задачи и ее перевод:
Advent of Code 2024, Day 14: Restroom Redoubt
--- День 14: Туалетный редут ---
Одному из Историков нужно в туалет; к счастью, вы знаете, что рядом с не посещенным местом в их списке есть туалет, и поэтому вас всех быстро телепортируют прямо в вестибюль штаб-квартиры Пасхального Кролика.
К сожалению, EBHQ, похоже, снова "улучшил" безопасность туалета после вашего последнего визита. Территория за пределами ванной кишит роботами!
Чтобы Историку безопасно добраться до ванной, вам понадобится способ предсказать, где роботы будут в будущем. К счастью, все они, похоже, движутся по кафельному полу по предсказуемым прямым линиям.
Вы создаете список (ваш пазл-вход) всех текущих позиций роботов ( p
) и скоростей ( v
), по одному роботу в строке. Например:
p=0,4 v=3,-3
p=6,3 v=-1,-3
p=10,3 v=-1,2
p=2,0 v=2,-1
p=0,0 v=1,3
p=3,0 v=-2,-2
p=7,6 v=-1,-3
p=3,0 v=-1,-2
p=9,3 v=2,3
p=7,3 v=-1,2
p=2,4 v=2,-3
p=9,5 v=-3,-3
Позиция каждого робота задается как , p=x,y
где x
представляет собой количество плиток, на которые робот отстоит от левой стены, а y
представляет собой количество плиток от верхней стены (если смотреть сверху). Таким образом, позиция p=0,0
означает, что робот находится в верхнем левом углу.
Скорость каждого робота задается как , v=x,y
где x
и y
задаются в плитках в секунду. Положительное x
значение означает, что робот движется вправо , а положительное y
значение означает, что робот движется вниз. Таким образом, скорость v=1,-2
означает, что каждую секунду робот перемещается 1
на плитку вправо и 2
на плитку вверх.
Роботы снаружи ванной комнаты находятся в пространстве 101
шириной и 103
высотой плиток (если смотреть сверху). Однако, в этом примере роботы находятся в пространстве 11
шириной и 7
высотой.
Роботы хорошо перемещаются друг над другом/под другом (благодаря комбинации пружин, выдвижных ног и квадрокоптеров), поэтому они могут делить одну и ту же плитку и не взаимодействовать друг с другом. Визуально количество роботов на каждой плитке в этом примере выглядит следующим образом:
1.12.......
...........
...........
......11.11
1.1........
.........1.
.......1...
Эти роботы обладают уникальной функцией для максимальной безопасности ванной комнаты: они могут телепортироваться. Когда робот сталкивается с краем пространства, в котором он находится, он вместо этого телепортируется на другую сторону, эффективно огибая края. Вот что p=2,4 v=2,-3
делает робот в течение первых нескольких секунд:
Исходное состояние:
...........
...........
...........
...........
..1........
...........
...........
После 1 секунды:
...........
....1......
...........
...........
...........
...........
...........
После 2 секунд:
...........
...........
...........
...........
...........
......1....
...........
После 3 секунд:
...........
...........
........1..
...........
...........
...........
...........
После 4 секунд:
...........
...........
...........
...........
...........
...........
..........1
После 5 секунд:
...........
...........
...........
.1.........
...........
...........
...........
Историк не может ждать долго, поэтому вам не придется долго моделировать роботов. Где будут роботы через 100
несколько секунд?
В приведенном выше примере количество роботов на каждой плитке по истечении 100 секунд выглядит следующим образом:
......2..1.
...........
1..........
.11........
.....1.....
...12......
.1....1....
Чтобы определить самую безопасную зону, подсчитайте количество роботов в каждом квадранте через 100 секунд. Роботы, которые находятся точно посередине (по горизонтали или вертикали), не считаются находящимися ни в одном квадранте, поэтому единственные соответствующие роботы:
..... 2..1.
..... .....
1.... .....
..... .....
...12 .....
.1... 1....
В этом примере квадранты содержат 1
, 3
, 4
, и 1
робот. Умножение их вместе дает общий коэффициент безопасности12
.
Предскажите движение роботов в вашем списке в пространстве 101
шириной в плитки и 103
высотой в плитки. Каким будет коэффициент безопасности после того, как пройдет ровно 100 секунд?
--- Часть вторая ---
Во время перерыва на туалет кто-то замечает, что эти роботы ужасно похожи на тех, что были построены и использовались на Северном полюсе. Если это роботы того же типа, у них должно быть жестко запрограммированное пасхальное яйцо: очень редко большинство роботов должны выстраиваться в изображение рождественской елки.
Какое наименьшее количество секунд должно пройти, чтобы роботы показали пасхальное яйцо?
Часть 1
Описанное в условии свойство "телепортации" робота описывает его поведение на поверхности тора.

В этом случае координаты каждого робота в любой момент времени легко вычисляются как "остаток от деления на" ширину/высоту площадки:
nx = x + (vx + w) * t) % w
ny = y + (vy + h) * t) % h
Поэтому самое сложное в нашем запросе - аккуратно вычислить количество роботов в каждом из квадрантов:
WITH arena AS (
SELECT
11 w
, 7 h
, 100 t
)
, bot AS (
SELECT
line[1]::bigint x
, line[2]::bigint y
, line[3]::bigint vx
, line[4]::bigint vy
FROM
regexp_matches($$
p=0,4 v=3,-3
p=6,3 v=-1,-3
p=10,3 v=-1,2
p=2,0 v=2,-1
p=0,0 v=1,3
p=3,0 v=-2,-2
p=7,6 v=-1,-3
p=3,0 v=-1,-2
p=9,3 v=2,3
p=7,3 v=-1,2
p=2,4 v=2,-3
p=9,5 v=-3,-3
$$
, 'p=(\d+),(\d+) v=(-?\d+),(-?\d+)' -- не забываем про возможные отрицательные значения
, 'g'
) line
)
SELECT -- количество роботов в каждом из квадрантов
count(*) FILTER(WHERE nx < w / 2 AND ny < h / 2) *
count(*) FILTER(WHERE nx < w / 2 AND ny > h / 2) *
count(*) FILTER(WHERE nx > w / 2 AND ny < h / 2) *
count(*) FILTER(WHERE nx > w / 2 AND ny > h / 2)
FROM
(
SELECT -- положения роботов в момент времени t
*
, (x + (vx + w) * t) % w nx
, (y + (vy + h) * t) % h ny
FROM
bot
, arena
) T;
По условию, ширина и высота площадки нечетны, поэтому w / 2
дает в PostgreSQL результат деления нацело, и для w = 101
вернет результат 50
, что как раз и является координатой разделяющей квадранты "средней линии".
Часть 2
Во второй части нас просят найти минимальный номер шага, на котором роботы выстраиваются в изображение-"пасхалку". Минимального, потому что из правил вычисления координат очевидно, что через каждые w * h
шагов состояния заведомо повторяются.
Точнее, конечно, через
НОК(w, h)
, но в нашем примере они заранее известны (101
и103
), и взаимно просты, поэтому - произведение.
Поскольку мы знаем, что роботы образуют некоторый "компактный" рисунок, то многие из них должны находиться недалеко друг от друга - то есть "вес" всей этой системы относительно центра масс должен быть минимальным.

При равных "массах", как в нашем случае, и так не слишком сложная формула становится еще немного попроще.
Переберем все w * h
возможных состояний и найдем шаг, на котором достигается минимум "веса" (вычислением квадратного корня для расстояния можно пренебречь):
WITH arena AS (
SELECT
11 w
, 7 h
)
, bot AS (
SELECT
line[1]::bigint x
, line[2]::bigint y
, line[3]::bigint vx
, line[4]::bigint vy
FROM
regexp_matches($$
p=0,4 v=3,-3
p=6,3 v=-1,-3
p=10,3 v=-1,2
p=2,0 v=2,-1
p=0,0 v=1,3
p=3,0 v=-2,-2
p=7,6 v=-1,-3
p=3,0 v=-1,-2
p=9,3 v=2,3
p=7,3 v=-1,2
p=2,4 v=2,-3
p=9,5 v=-3,-3
$$
, 'p=(\d+),(\d+) v=(-?\d+),(-?\d+)'
, 'g'
) line
)
, iter AS (
SELECT
t
, (
WITH state AS ( -- состояние роботов на момент t
SELECT
(x + (vx + w) * t) % w nx
, (y + (vy + h) * t) % h ny
FROM
bot
, arena
)
, center AS ( -- центр масс состояния
SELECT
avg(nx) cx
, avg(ny) cy
FROM
state
)
SELECT
sum((nx - cx) ^ 2 + (ny - cy) ^ 2) m -- общая масса системы
FROM
state
, center
)
FROM
arena
, generate_series(0, w * h - 1) t -- через w * h состояния заведомо повторяются
)
SELECT
t -- искомый момент
FROM
iter
ORDER BY
m -- минимальная масса системы среди всех итераций
LIMIT 1;
На реальных данных, где роботов 500
, а возможных состояний 101 * 103 = 10403
, моделирование занимает чуть меньше 11 секунд:

Итак, мы получили некоторое значение t
- давайте убедимся, что именно оно и дает искомую "пасхалку", подставив его в первый запрос и добавив визуализацию состояния:
...
SELECT
string_agg(
CASE WHEN state IS DISTINCT FROM NULL THEN '*' ELSE ' ' END, ''
ORDER BY x
) собираем строку из упорядоченных по x символов
FROM
( -- генерируем все клетки
SELECT
x
, y
FROM
arena
, generate_series(0, w - 1) x
, generate_series(0, h - 1) y
) map
LEFT JOIN
state
ON (nx, ny) = (x, y)
GROUP BY
y
ORDER BY
y; -- выводим строки по порядку
Если мы все сделали правильно, то результат будет примерно таким:
*
* * * *
*
*
*
*
*
* *
* *
* * *
* * *
* *
* *
* *
*
* *
* *
*
* * * * *
* *
*
* *
* * * *
** * *
* * * * *
* *
* *
*
*
* *******************************
* *
* *
* *
* *
* * * *
* * *** * *
* ***** *
* ******* *
* ********* * * *
* ***** * *
* ******* *
* ********* * *
* *********** * *
* ************* *
* ********* *
* *********** * * * *
* * ************* *
* *************** *
* ***************** * *
* ************* * * * *
* *************** * * *
* * * ***************** *
** * ******************* *
* ********************* * *
* *** * *
* *** * *
* *** *
* * *
* *
* * * *
* * *
******************************* * *
*
* *
* * *
*
* * **
* *
*
* *
*
*
* *
* *
*
*
*
*
* * * *
*
*
* * *
*
* *
* *
* *
* * *
* * *
*
* *
*
* *