Как стать автором
Обновить
173.42
Тензор
Разработчик системы Saby

SQL HowTo: поиск пути и дихотомия (Advent of Code 2024, Day 18: RAM Run)

Уровень сложностиПростой
Время на прочтение8 мин
Количество просмотров386

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

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

Сегодня напишем для решения простую реализацию алгоритма Ли и дихотомии.


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

Advent of Code 2024, Day 18: RAM Run

--- День 18: Пробег RAM ---

Вы и Историки выглядите гораздо более пикселизированными, чем вы помните. Вы внутри компьютера на Северном полюсе!

Как раз когда вы собираетесь осмотреть окрестности, к вам подбегает программа. «Эта область памяти небезопасна! Пользователь неправильно понял, что такое pushdown-автомат, и его алгоритм выталкивает на нас целые байты! Бегите!»

Алгоритм быстр - он будет заставлять байт попадать в ваше пространство памяти раз в наносекунду! К счастью, вы быстрее, и, быстро сканируя алгоритм, вы создаете список байтов, в том порядке, в котором они попадут в ваше пространство памяти (ваши входные данные для головоломки).

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

5,4
4,2
4,5
3,0
2,1
6,3
2,4
1,5
0,6
3,3
2,6
5,1
1,2
5,5
2,5
6,5
1,4
0,4
6,4
1,1
6,1
1,0
0,5
1,6
2,0

Каждая позиция байта задается в виде координат X,Y, где X- расстояние от левого края области памяти, а Y - расстояние от верхнего края области памяти.

Вы и Историки в настоящее время находитесь в верхнем левом углу пространства памяти (в 0,0) и должны достичь выхода в нижнем правом углу (70,70 в вашем пространстве памяти, но 6,6 в этом примере). Вам нужно будет смоделировать падающие байты, чтобы спланировать, где будет безопасно бежать; для начала смоделируйте только первые несколько байтов, падающих в ваше пространство памяти.

Когда байты попадают в ваше пространство памяти, они делают эту координату поврежденной. Поврежденные координаты памяти не могут быть введены вами или Историками, поэтому вам нужно будет тщательно спланировать свой маршрут. Вы также не можете покинуть границы пространства памяти; ваша единственная надежда - добраться до выхода.

В приведенном выше примере, если бы вы изобразили пространство памяти после того, как первые 12 байтов попали (используя . для безопасных и # для поврежденных), это выглядело бы так:

...#...
..#..#.
....#..
...#..#
..#..#.
.#..#..
#.#....

Вы можете делать шаги вверх, вниз, влево или вправо. После того, как всего 12 байтов испортили ячейки в вашем пространстве памяти, кратчайший путь от верхнего левого угла до выхода будет содержать 22 шага. Вот один из таких путей (отмечен O):

OO.#OOO
.O#OO#O
.OOO#OO
...#OO#
..#OO#.
.#.O#..
#.#OOOO

Имитируйте первый килобайт (1024 байт), падающий на ваше пространство памяти. После этого, какое минимальное количество шагов необходимо, чтобы достичь выхода?

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

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

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

В приведенном выше примере после того, как байт падает в 1,1, все еще остается путь к выходу:

O..#OOO
O##OO#O
O#OO#OO
OOO#OO#
###OO##
.##O###
#.#OOOO

Однако после добавления следующего байта (в точке 6,1) пути к выходу больше нет:

...#...
.##..##
.#..#..
...#..#
###..##
.##.###
#.#....

Итак, в этом примере координаты первого байта, который делает выход недоступным, равны 6,1.

Смоделируйте больше байтов, которые вот-вот испортят ваше пространство памяти. Каковы координаты первого байта, который сделает выход недоступным из вашей начальной позиции? (Укажите ответ в виде двух целых чисел, разделенных запятой, без других символов.)

Часть 1

Итак, для решения нам требуется найти кратчайший путь между начальной и конечной клетками на карте с препятствиями. Для этого есть классическое решение - волновой алгоритм Ли. Очень похожую реализацию я использовал ранее для генерации лабиринтов.

Сначала определим координаты угловых стартовой и финальной точек и "арену":

WITH RECURSIVE src AS (
  SELECT
    '(0,0)' s
  , '(6,6)' e
)
, arena AS (
  SELECT
    box(
      s::point
    , e::point
    ) -- прямоугольник с углами (0,0) и (N,N)
  FROM
    src
)

Соберем все "выпадающие" байты в массив с тестовыми представлениями координат клеток:

, bytes AS (
  SELECT
    array_agg(point(line[1]::integer, line[2]::integer)::text) corrupted -- массив координат "текстом"
  FROM
    regexp_matches($$
5,4
4,2
4,5
3,0
2,1
6,3
2,4
1,5
0,6
3,3
2,6
5,1
1,2
5,5
2,5
6,5
1,4
0,4
6,4
1,1
6,1
1,0
0,5
1,6
2,0
$$
  , '(\d+),(\d+)'
  , 'g') line
)
corrupted
{"(5,4)","(4,2)","(4,5)","(3,0)","(2,1)"
,"(6,3)","(2,4)","(1,5)","(0,6)","(3,3)"
,"(2,6)","(5,1)","(1,2)","(5,5)","(2,5)"
,"(6,5)","(1,4)","(0,4)","(6,4)","(1,1)"
,"(6,1)","(1,0)","(0,5)","(1,6)","(2,0)"}

Осталось написать рекурсивное заполнение нашей карты из стартовой точки. Для оптимизации нам понадобится разделить уже "залитые" клетки (fill) и "фронт волны" (wave), из которого можно добраться до новых клеток.

В качестве представления координат каждой из клеток будем использовать тип point и его текстовое представление:

, r AS (
  SELECT
    0 i
  , ARRAY[s::text] fill -- уже "залитые" клетки
  , ARRAY[s::text] wave -- "фронт" волны
  FROM
    src
UNION ALL
  SELECT
    i + 1
  , ARRAY(
      SELECT DISTINCT
        unnest
      FROM
        unnest(r.fill || T.wave)
    ) -- объединяем с уникализацией множества ранее и сейчас достигнутых клеток
  , T.wave
  FROM
    r
  , src
  , arena
  , bytes
  , LATERAL (
      SELECT
        array_agg(DISTINCT np::text) wave -- уникализируем текстовые представления "точек"
      FROM
        unnest(r.wave) cell -- "разворачиваем" массив координат клеток "волны"
      , point(cell) p       -- преобразуем каждую пару координат в геометрическую точку
      , LATERAL (           -- определяем доступные ячейки из текущей
          VALUES
            (p + '(1,0)'::point)  -- справа
          , (p + '(0,1)'::point)  -- снизу
          , (p + '(-1,0)'::point) -- слева
          , (p + '(0,-1)'::point) -- сверху
        ) n(np)
      WHERE
        np <@ box AND                       -- новая клетка должна лежать в границах арены
        np::text <> ALL(corrupted[:12]) AND -- не принадлежать нужному набору "поврежденных"
        np::text <> ALL(r.fill)             -- и не входить в список уже достигнутых ранее
    ) T
  WHERE
    e <> ALL(r.wave) -- "гоним волну", пока конечная точка не окажется среди достигнутых
)

Если изобразить номер шага на карте из примера "после 12 байтов", получится примерно так:

012#89A
12#67#B
2345#DC
345#FE#
45#HG#.
5#JI#M.
#.#JKLM

Для решения задачи нам осталось лишь найти максимальный номер шага:

WITH RECURSIVE src AS (
  SELECT
    '(0,0)' s
  , '(6,6)' e
)
, arena AS (
  SELECT
    box(
      s::point
    , e::point
    ) -- прямоугольник с углами (0,0) и (N,N)
  FROM
    src
)
, bytes AS (
  SELECT
    array_agg(point(line[1]::integer, line[2]::integer)::text) corrupted -- массив координат "текстом"
  FROM
    regexp_matches($$
5,4
4,2
4,5
3,0
2,1
6,3
2,4
1,5
0,6
3,3
2,6
5,1
1,2
5,5
2,5
6,5
1,4
0,4
6,4
1,1
6,1
1,0
0,5
1,6
2,0
$$
  , '(\d+),(\d+)'
  , 'g') line
)
, r AS (
  SELECT
    0 i
  , ARRAY[s::text] fill -- уже "залитые" клетки
  , ARRAY[s::text] wave -- "фронт" волны
  FROM
    src
UNION ALL
  SELECT
    i + 1
  , ARRAY(
      SELECT DISTINCT
        unnest
      FROM
        unnest(r.fill || T.wave)
    ) -- объединяем с уникализацией множества ранее и сейчас достигнутых клеток
  , T.wave
  FROM
    r
  , src
  , arena
  , bytes
  , LATERAL (
      SELECT
        array_agg(DISTINCT np::text) wave -- уникализируем текстовые представления "точек"
      FROM
        unnest(r.wave) cell -- "разворачиваем" массив координат клеток "волны"
      , point(cell) p       -- преобразуем каждую пару координат в геометрическую точку
      , LATERAL (           -- определяем доступные ячейки из текущей
          VALUES
            (p + '(1,0)'::point)  -- справа
          , (p + '(0,1)'::point)  -- снизу
          , (p + '(-1,0)'::point) -- слева
          , (p + '(0,-1)'::point) -- сверху
        ) n(np)
      WHERE
        np <@ box AND                       -- новая клетка должна лежать в границах арены
        np::text <> ALL(corrupted[:12]) AND -- не принадлежать нужному набору "поврежденных"
        np::text <> ALL(r.fill)             -- и не входить в список уже достигнутых ранее
    ) T
  WHERE
    e <> ALL(r.wave) -- "гоним волну", пока конечная точка не окажется среди достигнутых
)
SELECT
  max(i)
FROM
  r;

На реальных данных (карта 70x70 и 1024 первых байта) решение занимает чуть меньше 3 секунд:

Часть 2

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

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

В этом случае нам потребуется проверить не более log2(N) вариантов - то есть, грубо можно оценить для входного набора как 12 * 3 = 36 секунд.

Сделаем внешний рекурсивный "цикл", реализующий дихотомию:

, ro AS (
  SELECT
    0 step
  , 0 lr
  , array_length(corrupted, 1) rr
  FROM
    bytes
UNION ALL
  SELECT
    step + 1
  , CASE WHEN cond THEN (lr + rr) >> 1 ELSE lr END
  , CASE WHEN cond THEN rr ELSE ((lr + rr) >> 1) END
  FROM
    ro
  , LATERAL (
      WITH RECURSIVE r AS (
        ...
              np::text <> ALL(corrupted[:((lr + rr) >> 1)]) AND -- нужное количество первых байт
        ...
      )
      SELECT
        e = ANY(wave) cond -- достигли ли мы конечной точки на последнем шаге рекурсии?
      FROM
        r
      , src
      ORDER BY
        i DESC
      LIMIT 1
    ) T
  WHERE
    lr < rr - 1 -- пока левая и правая границы не стали соседями
)
step | lr | rr
   0 |  0 | 25 -- ( 0 + 25) >> 1 = 12 - выход доступен, идем вправо
   1 | 12 | 25 -- (12 + 25) >> 1 = 18 - выход доступен, идем вправо
   2 | 18 | 25 -- (18 + 25) >> 1 = 21 - выход недоступен, идем влево
   3 | 18 | 21 -- (18 + 21) >> 1 = 19 - выход доступен, идем вправо
   4 | 19 | 21 -- (19 + 21) >> 1 = 20 - выход доступен, идем вправо
   5 | 20 | 21 -- зажато - при 20 выход доступен, при 21 - уже нет

Нам будет нужен байт, находящийся в массиве на позиции rr с последнего шага, ну и немного почистить лишние символы:

SELECT
  regexp_replace(
    corrupted[(
      SELECT
        rr
      FROM
        ro
      ORDER BY
        step DESC
      LIMIT 1
    )]
  , '[\(\)]' -- убираем лишние скобки
  , ''
  , 'g')
FROM
  bytes;

Полный вариант запроса:

WITH RECURSIVE src AS (
  SELECT
    '(0,0)' s
  , '(6,6)' e
)
, arena AS (
  SELECT
    box(
      s::point
    , e::point
    ) -- прямоугольник с углами (0,0) и (N,N)
  FROM
    src
)
, bytes AS (
  SELECT
    array_agg(point(line[1]::integer, line[2]::integer)::text) corrupted -- массив координат "текстом"
  FROM
    regexp_matches($$
5,4
4,2
4,5
3,0
2,1
6,3
2,4
1,5
0,6
3,3
2,6
5,1
1,2
5,5
2,5
6,5
1,4
0,4
6,4
1,1
6,1
1,0
0,5
1,6
2,0
$$
  , '(\d+),(\d+)'
  , 'g') line
)
, ro AS (
  SELECT
    0 step
  , 0 lr
  , array_length(corrupted, 1) rr
  FROM
    bytes
UNION ALL
  SELECT
    step + 1
  , CASE WHEN cond THEN (lr + rr) >> 1 ELSE lr END
  , CASE WHEN cond THEN rr ELSE ((lr + rr) >> 1) END
  FROM
    ro
  , LATERAL (
      WITH RECURSIVE r AS (
        SELECT
          0 i
        , ARRAY[s::text] fill -- уже "залитые" клетки
        , ARRAY[s::text] wave -- "фронт" волны
        FROM
          src
      UNION ALL
        SELECT
          i + 1
        , ARRAY(
            SELECT DISTINCT
              unnest
            FROM
              unnest(r.fill || T.wave)
          ) -- объединяем с уникализацией множества ранее и сейчас достигнутых клеток
        , T.wave
        FROM
          r
        , src
        , arena
        , bytes
        , LATERAL (
            SELECT
              array_agg(DISTINCT np::text) wave -- уникализируем текстовые представления "точек"
            FROM
              unnest(r.wave) cell -- "разворачиваем" массив координат клеток "волны"
            , point(cell) p       -- преобразуем каждую пару координат в геометрическую точку
            , LATERAL (           -- определяем доступные ячейки из текущей
                VALUES
                  (p + '(1,0)'::point)  -- справа
                , (p + '(0,1)'::point)  -- снизу
                , (p + '(-1,0)'::point) -- слева
                , (p + '(0,-1)'::point) -- сверху
              ) n(np)
            WHERE
              np <@ box AND                       -- новая клетка должна лежать в границах арены
              np::text <> ALL(corrupted[:((lr + rr) >> 1)]) AND -- нужное количество первых байт
              np::text <> ALL(r.fill)             -- и не входить в список уже достигнутых ранее
          ) T
        WHERE
          e <> ALL(r.wave) -- "гоним волну", пока конечная точка не окажется среди достигнутых
      )
      SELECT
        e = ANY(wave) cond -- достигли ли мы конечной точки на последнем шаге рекурсии?
      FROM
        r
      , src
      ORDER BY
        i DESC
      LIMIT 1
    ) T
  WHERE
    lr < rr - 1 -- пока левая и правая границы не стали соседями
)
SELECT
  regexp_replace(
    corrupted[(
      SELECT
        rr
      FROM
        ro
      ORDER BY
        step DESC
      LIMIT 1
    )]
  , '[\(\)]' -- убираем лишние скобки
  , ''
  , 'g')
FROM
  bytes;

Вместо предельных 36 секунд реальное решение заняло даже меньше 8 секунд:

Теги:
Хабы:
+11
Комментарии0

Публикации

Информация

Сайт
saby.ru
Дата регистрации
Дата основания
Численность
1 001–5 000 человек
Местоположение
Россия