SQL HowTo: моделирование против подсчета (Advent of Code 2024, Day 21: Keypad Conundrum)
В этой челлендж-серии статей попробуем использовать PostgreSQL как среду для решения задач Advent of Code 2024.
Возможно, SQL не самый подходящий для этого язык, зато мы рассмотрим его различные возможности, о которых вы могли и не подозревать.
Пробуем смоделировать преобразования строк «в лоб», а потом — организовать подсчет и решить более сложную задачу в разы быстрее простой!
Решение Day 1: Historian Hysteria (регулярные выражения и условная агрегация)
Решение Day 2: Red-Nosed Reports (логические агрегаты)
Решение Day 3: Mull It Over («чистые» регулярки)
Решение Day 4: Ceres Search (работа с массивами)
Решение Day 5: Print Queue (поиск в словаре и массивах, сортировка «пузырьком»)
Решение Day 6: Guard Gallivant (рекурсивные циклы и их контроль)
Решение Day 7: Bridge Repair («экспоненциальная» рекурсия)
Решение Day 8: Resonant Collinearity (генерация и подсчет уникальных комбинаций)
Решение Day 9: Disk Fragmenter (оптимизируем рекурсию)
Решение Day 10: Hoof It (поиск «в ширину» внутри цикла)
Решение Day 11: Plutonian Pebbles (агрегация внутри рекурсии)
Решение Day 12: Garden Groups (волновой алгоритм и подсчет границ)
Решение Day 13: Claw Contraption (пошагово решаем СЛУ)
Решение Day 14: Restroom Redoubt (находим «елочку» с помощью центра масс)
Решение Day 15: Warehouse Woes (играем в сокобан с помощью json-карты и типа point)
Решение Day 16: Reindeer Maze (укрощаем рекурсию в лабиринте)
Решение Day 17: Chronospatial Computer (подбираем значение ветвлением)
Решение Day 18: RAM Run (поиск пути и дихотомия)
Решение Day 19: Linen Layout (динамическое программирование)
Решение Day 20: Race Condition (кратчайший путь «туда и обратно» и его самосоединение)
Решение Day 21: Keypad Conundrum (моделирование против подсчета)

Оригинальная постановка задачи и ее перевод:
--- День 21: Головоломка с клавиатурой ---
Когда вы телепортируетесь на звездолет Санты класса «Олень», Историки начинают паниковать: кто-то из их поисковой группы пропал. Быстрое сканирование жизненных форм корабельным компьютером показывает, что когда пропавший Историк телепортировался, он оказался в другой части корабля.
Дверь в эту зону заперта, но компьютер не может ее открыть; ее можно открыть, только физически введя коды двери (данные головоломки) на цифровой клавиатуре на двери.
Цифровая клавиатура имеет четыре ряда кнопок: 789
, 456
, 123
, и, наконец, пустой пробел, за которым следует 0A
. Визуально они расположены следующим образом:
+---+---+---+
| 7 | 8 | 9 |
+---+---+---+
| 4 | 5 | 6 |
+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
| 0 | A |
+---+---+
К сожалению, пространство за дверью в настоящее время разгерметизировано, и никто не может приблизиться к двери. Вместо этого нужно отправить робота.
У робота нет проблем с навигацией по кораблю и поиском цифровой клавиатуры, но он не предназначен для нажатия кнопок: ему нельзя напрямую приказать нажать определенную кнопку. Вместо этого у него есть роботизированная рука, которой можно управлять дистанционно с помощью направляющей клавиатуры.
Направляющая клавиатура имеет два ряда кнопок: пробел / ^
(вверх) / A
(активировать) в первом ряду и <
(влево) / v
(вниз) / >
(вправо) во втором ряду. Визуально они расположены следующим образом:
+---+---+
| ^ | A |
+---+---+---+
| < | v | > |
+---+---+---+
Когда робот достигает цифровой клавиатуры, его роботизированная рука указывает на кнопку A
в правом нижнем углу. После этого необходимо использовать этот пульт дистанционного управления с направляющей клавиатурой для маневрирования роботизированной рукой: кнопки вверх / вниз / влево / вправо заставляют его перемещать руку на одну кнопку в этом направлении, а кнопка A
заставляет робота кратковременно двигаться вперед, нажимая кнопку, на которую нацелена роботизированная рука.
Например, чтобы заставить робота печатать 029A
на цифровой клавиатуре, можно использовать следующую последовательность ввода на клавиатуре направлений:
<
, чтобы переместить руку изA
(исходного положения) в0
.A
, чтобы нажать кнопку0
.^A
, чтобы переместить руку к кнопке2
и нажать ее.>^^A
, чтобы переместить руку к кнопке9
и нажать ее.vvvA
переместить руку к кнопкеA
и нажать ее.
Всего существует три кратчайшие возможные последовательности нажатий кнопок на этой навигационной клавиатуре, которые заставят робота набрать 029A
: ^^AvvvA
, ^AvvvA
, и AvvvA
.
К сожалению, область, в которой находится этот пульт дистанционного управления с навигационной клавиатурой, в настоящее время испытывает высокий уровень радиации, и никто не может приблизиться к нему. Вместо этого нужно отправить робота.
Когда робот достигает навигационной клавиатуры, его рука робота направлена на кнопку A
в правом верхнем углу. После этого для управления этим роботом используется второй, другой навигационный пульт дистанционного управления (так же, как и первый робот, за исключением того, что этот печатает на навигационной клавиатуре вместо цифровой).
Существует несколько кратчайших возможных последовательностей нажатий кнопок направленной клавиатуры, которые заставят этого робота сказать первому роботу набрать текст 029A
на двери. Одна из таких последовательностей — v<>^AAvA<^AA>A
.
>^AvAA<^A>A >^AvA^A ^A ^A>AAvA^A A>^AAAvA<^A>A v< >^AAvA<^AA>A
^A ^^AvvvA 029A
Подводя итог, можно сказать, что существуют следующие клавиатуры:
Чтобы открыть дверь, нужно будет ввести пять кодов на ее цифровой клавиатуре. Например:
029A
980A
179A
456A
379A
029A:
>^AvAA<^A>A >^AvA^A ^A ^A>AAvA^A A>^AAAvA<^A>A 980A: >^AAAvA^A >^AvAA<^A>A A>^AAAvA<^A>A ^A A 179A:
>^A A>^AAvAA<^A>A >^AAvA^A ^AA A>^AAAvA<^A>A 456A: A>^AA >^AAvAA<^A>A ^A ^A AA>^AAvA<^A>A 379A: A>^AvA^A >^AAvA<^A>AAvA^A ^AA A>^AAAvA<^A>A
Сложность одного кода (например, 029A
) равна результату умножения двух значений:
--- Часть вторая ---
На этот раз задействовано гораздо больше роботов. Вкратце, есть следующие клавиатуры:
Часть 1
Сначала, уже привычными регулярными выражениями, разложим список имеющихся кодов в строки:
WITH src AS (
SELECT
line[1] code
FROM
regexp_matches(
$$
029A
980A
179A
456A
379A
$$
, '[^\r\n]+(?=$|[\r\n])'
, 'g'
) line
)
Теперь преобразуем макет цифровой клавиатуры в список кнопок с координатами:
, numpad AS (
SELECT
d[1] k
, x
, y
FROM
regexp_matches(
$$
789
456
123
0A
$$
, '[^\r\n]+(?=$|[\r\n])'
, 'g'
)
WITH ORDINALITY y(line, y)
, regexp_matches(
line[1]
, '.'
, 'g'
)
WITH ORDINALITY x(d, x)
WHERE
d[1] <> ' '
)
k | x | y
7 | 1 | 1
8 | 2 | 1
9 | 3 | 1
4 | 1 | 2
5 | 2 | 2
6 | 3 | 2
1 | 1 | 3
2 | 2 | 3
3 | 3 | 3
0 | 2 | 4
A | 3 | 4
, numpad_step AS (
SELECT DISTINCT
kb1.k || kb2.k pair
, unnest(ARRAY[
-- сначала горизонтально, потом вертикально
repeat(CASE WHEN kb2.x > kb1.x THEN '>' ELSE '<' END, abs(kb2.x - kb1.x)::integer)
|| repeat(CASE WHEN kb2.y > kb1.y THEN 'v' ELSE '^' END, abs(kb2.y - kb1.y)::integer)
-- или сначала вертикально, потом горизонтально
, repeat(CASE WHEN kb2.y > kb1.y THEN 'v' ELSE '^' END, abs(kb2.y - kb1.y)::integer)
|| repeat(CASE WHEN kb2.x > kb1.x THEN '>' ELSE '<' END, abs(kb2.x - kb1.x)::integer)
]) || 'A' code
FROM
numpad kb1
, numpad kb2
EXCEPT ALL -- исключаем пути через "пустое место"
VALUES
('01', '<^A')
, ('04', '<^^A')
, ('07', '<^^^A')
, ('A1', '<<^A')
, ('A4', '<<^^A')
, ('A7', '<<^^^A')
, ('10', 'v>A')
, ('40', 'vv>A')
, ('70', 'vvv>A')
, ('1A', 'v>>A')
, ('4A', 'vv>>A')
, ('7A', 'vvv>>A')
)
pair | code 00 | A 01 | ^
^A 03 | ^>A 04 | ^^A 06 | >^^A 07 | ^^^^^^A 09 | ^^^>A 0A | >A ...
, arrpad AS (
SELECT
d[1] k
, x
, y
FROM
regexp_matches(
$$
^A
$$
, '[^\r\n]+(?=$|[\r\n])'
, 'g'
)
WITH ORDINALITY y(line, y)
, regexp_matches(
line[1]
, '.'
, 'g'
)
WITH ORDINALITY x(d, x)
WHERE
d[1] <> ' '
)
, arrpad_step AS (
SELECT DISTINCT
kb1.k || kb2.k pair
, unnest(ARRAY[
repeat(CASE WHEN kb2.x > kb1.x THEN '>' ELSE '<' END, abs(kb2.x - kb1.x)::integer)
|| repeat(CASE WHEN kb2.y > kb1.y THEN 'v' ELSE '^' END, abs(kb2.y - kb1.y)::integer)
, repeat(CASE WHEN kb2.y > kb1.y THEN 'v' ELSE '^' END, abs(kb2.y - kb1.y)::integer)
|| repeat(CASE WHEN kb2.x > kb1.x THEN '>' ELSE '<' END, abs(kb2.x - kb1.x)::integer)
]) || 'A' code
FROM
arrpad kb1
, arrpad kb2
EXCEPT ALL
VALUES
('<^', '^>A')
, ('>A')
, ('^<', '
SELECT
*
FROM
src
, LATERAL (
WITH RECURSIVE step AS ( -- превращаем код в цепочку переходов между парами кнопок
SELECT
i
, substr('A' || code, i, 2) pair
FROM
generate_series(1, length(code)) i
)
, r AS ( -- рекурсивно пробуем разные варианты подстановок
SELECT
1 i
, '' code0
UNION ALL
SELECT
i + 1
, code0 || code
FROM
r
JOIN
step
USING(i)
JOIN
numpad_step -- цифровая клавиатура
USING(pair)
)
TABLE r
ORDER BY
i DESC
FETCH FIRST 1 ROW -- оставляем только строки с последнего шага
WITH TIES
) T0
code | i | code0 029A | 5 | AvvvA 029A | 5 | ^^AvvvA 980A | 5 | ^^^A
A 179A | 5 | ^<>AvvvA 456A | 5 | ^^<
A>AvvA 379A | 5 | ^A<<^^A>>AvvvA 379A | 5 | ^A^^<>AvvvA
code | i | code0 | i | code1 | i | code2
029A | 5 | AvvvA | 13 | v<
>^AAA^AvA | 29 | v>^AvAA^ Av<>^AvA^Av<>^AAvA^AAv>^AAAA^A
029A | 5 | AvvvA | 13 | v< >^AAA^AvA | 29 | >^AvAA<^A>Av< >^AvA^Av<>^AAA^A A>^AAA vA^A
029A | 5 | AvvvA | 13 | v<>^AAA^AvA | 29 | v>^AvAA<^A>Av< >^AvA^Av<>^AAA^A A>^AAA vA^A
...
SELECT
sum(regexp_replace(code, '\D', '', 'g')::integer * length(code2))
FROM
(
SELECT DISTINCT ON(code)
...
ORDER BY
code, length(code2)
) T;
Остается лишь сложить все вместе и получить ответ:
WITH src AS (
SELECT
line[1] code
FROM
regexp_matches(
$$
029A
980A
179A
456A
379A
$$
, '[^\r\n]+(?=$|[\r\n])'
, 'g'
) line
)
, numpad AS (
SELECT
d[1] k
, x
, y
FROM
regexp_matches(
$$
789
456
123
0A
$$
, '[^\r\n]+(?=$|[\r\n])'
, 'g'
)
WITH ORDINALITY y(line, y)
, regexp_matches(
line[1]
, '.'
, 'g'
)
WITH ORDINALITY x(d, x)
WHERE
d[1] <> ' '
)
, numpad_step AS (
SELECT DISTINCT
kb1.k || kb2.k pair
, unnest(ARRAY[
repeat(CASE WHEN kb2.x > kb1.x THEN '>' ELSE '<' END, abs(kb2.x - kb1.x)::integer) || repeat(CASE WHEN kb2.y > kb1.y THEN 'v' ELSE '^' END, abs(kb2.y - kb1.y)::integer)
, repeat(CASE WHEN kb2.y > kb1.y THEN 'v' ELSE '^' END, abs(kb2.y - kb1.y)::integer) || repeat(CASE WHEN kb2.x > kb1.x THEN '>' ELSE '<' END, abs(kb2.x - kb1.x)::integer)
]) || 'A' code
FROM
numpad kb1
, numpad kb2
EXCEPT ALL
VALUES
('01', '<^A')
, ('04', '<^^A')
, ('07', '<^^^A')
, ('A1', '<<^A')
, ('A4', '<<^^A')
, ('A7', '<<^^^A')
, ('10', 'v>A')
, ('40', 'vv>A')
, ('70', 'vvv>A')
, ('1A', 'v>>A')
, ('4A', 'vv>>A')
, ('7A', 'vvv>>A')
)
, arrpad AS (
SELECT
d[1] k
, x
, y
FROM
regexp_matches(
$$
^A
$$
, '[^\r\n]+(?=$|[\r\n])'
, 'g'
)
WITH ORDINALITY y(line, y)
, regexp_matches(
line[1]
, '.'
, 'g'
)
WITH ORDINALITY x(d, x)
WHERE
d[1] <> ' '
)
, arrpad_step AS (
SELECT DISTINCT
kb1.k || kb2.k pair
, unnest(ARRAY[
repeat(CASE WHEN kb2.x > kb1.x THEN '>' ELSE '<' END, abs(kb2.x - kb1.x)::integer) || repeat(CASE WHEN kb2.y > kb1.y THEN 'v' ELSE '^' END, abs(kb2.y - kb1.y)::integer)
, repeat(CASE WHEN kb2.y > kb1.y THEN 'v' ELSE '^' END, abs(kb2.y - kb1.y)::integer) || repeat(CASE WHEN kb2.x > kb1.x THEN '>' ELSE '<' END, abs(kb2.x - kb1.x)::integer)
]) || 'A' code
FROM
arrpad kb1
, arrpad kb2
EXCEPT ALL
VALUES
('<^', '^>A')
, ('>A')
, ('^<', '
Ничего хоть сколько-то сложного в нашем запросе нет, поэтому результат мы получим всего за 120 мс:

Часть 2
Во второй части нам предлагается сделать все ровно то же самое, только роботов с направляющей клавиатурой будет не 2, а 25. Казалось бы, копипаста рулит!… Но нет — длина каждой строки растет экспоненциально, и даже для 10 роботов дождаться уже нереально.
Для начала заметим, что на направляющей клавиатуре, помимо «невозможных» (проходящих через пустую позицию) есть и «заведомо неэффективные», которые через 1–2 преобразования дают заведомо более длинные последовательности — исключим их сразу:
, arrpad_step AS (
SELECT DISTINCT
kb1.k || kb2.k pair
, unnest(ARRAY[
repeat(CASE WHEN kb2.x > kb1.x THEN '>' ELSE '<' END, abs(kb2.x - kb1.x)::integer) || repeat(CASE WHEN kb2.y > kb1.y THEN 'v' ELSE '^' END, abs(kb2.y - kb1.y)::integer)
, repeat(CASE WHEN kb2.y > kb1.y THEN 'v' ELSE '^' END, abs(kb2.y - kb1.y)::integer) || repeat(CASE WHEN kb2.x > kb1.x THEN '>' ELSE '<' END, abs(kb2.x - kb1.x)::integer)
]) || 'A' code
FROM
arrpad kb1
, arrpad kb2
EXCEPT ALL
VALUES
('<^', '^>A')
, ('>A')
, ('^<', '^', '^', '>vA')
, ('Av', 'v^A')
)
Теперь у наше преобразование на каждом из шагов набора на направляющей клавиатуре стало однозначным — 5 кнопок дают 25 возможных переходов между ними:
pair | code
<< | A
<> | >>A
<^ | >^A
>< | <
> | A
>^ | <^A
^< | v | v>A
^^ | A
>^A
>A | ^A
^A | >A
A< | v< | vA
A^ | A
>v | | >A
v^ | ^A
vA | ^>A
vv | A
, map AS (
SELECT
s.pair src
, jsonb_object(
array_agg(T.t)
, array_agg(T.q)::text[]
) trg
FROM
arrpad_step s
, LATERAL (
SELECT
substr('A' || code, i, 2) t
, count(*) q
FROM
generate_series(1, length(code)) i
GROUP BY
1
) T
GROUP BY
1
)
src | trg
<< | {"AA": "1"}
<> | {">>": "1", ">A": "1", "A>": "1"}
<^ | {">^": "1", "A>": "1", "^A": "1"}
>< | {"<<": "1", "> | {"AA": "1"}
>^ | {"<^": "1", "A<": "1", "^A": "1"}
^< | {" | {">A": "1", "Av": "1", "v>": "1"}
^^ | {"AA": "1"}
>": "1", ">^": "1", "A>": "1", "^A": "1"}
>A | {"A^": "1", "^A": "1"}
^A | {">A": "1", "A>": "1"}
A< | {"<<": "1", " | {"Av": "1", "vA": "1"}
A^ | {"A": "1", "A>": "1"}
>v | {" | {">A": "1", "A>": "1"}
v^ | {"A^": "1", "^A": "1"}
vA | {">A": "1", "A^": "1", "^>": "1"}
vv | {"AA": "1"}
Осталось лишь написать рекурсивное «погружение» на нужное количество шагов:
, r AS (
SELECT
0 bot
, code src
, trg
FROM
src
, LATERAL ( -- преобразуем числовой код в варианты последовательностей стрелок
WITH RECURSIVE step AS (
SELECT
i
, substr('A' || code, i, 2) pair
FROM
generate_series(1, length(code)) i
)
, r AS (
SELECT
1 i
, '' code0
UNION ALL
SELECT
i + 1
, code0 || code
FROM
r
JOIN
step
USING(i)
JOIN
numpad_step
USING(pair)
)
TABLE r
ORDER BY
i DESC
FETCH FIRST 1 ROW
WITH TIES
) T0
, LATERAL ( -- последовательность стрелок превращаем в набор парных переходов
SELECT
jsonb_object(
array_agg(T.t)
, array_agg(T.q)::text[]
) trg
FROM
(
SELECT
substr('A' || code0, i, 2) t
, count(*) q
FROM
generate_series(1, length(code0)) i
GROUP BY
1
) T
) T
UNION ALL
SELECT
bot + 1
, r.src
, T.*
FROM
r
, LATERAL (
SELECT
jsonb_object(
array_agg(k)
, array_agg(v)::text[]
) trg -- собираем счетчики
FROM
(
SELECT
k1 k
, sum(v0::bigint * v1::bigint) v
FROM
jsonb_each_text(r.trg) T0(k0, v0) -- разворачиваем ключ-значение текущего шага
JOIN
map
ON map.src = k0 -- находим, во что превратится каждый ключ
, jsonb_each_text(map.trg) T1(k1, v1) -- разворачиваем ключ-значение следующего шага
GROUP BY
1
) T
) T
WHERE
bot < 25
)
bot | src | trg
0 | 029A | {"^": "1", "A<": "1", "A>": "1", "A^": "1", "Av": "1", "^A": "2", "^^": "1", "vA": "1", "vv": "2"}
0 | 029A | {"A": "1", "A<": "1", "A^": "2", "Av": "1", "^>": "1", "^A": "1", "^^": "1", "vA": "1", "vv": "2"}
0 | 179A | {"<<": "1", ">": "1", ">A": "1", "A>": "1", "A^": "2", "Av": "1", "^<": "1", "^A": "1", "^^": "1", "vA": "1", "vv": "2"}
0 | 379A | {"<<": "1", "<^": "1", ">>": "1", ">A": "1", "A<": "1", "A>": "1", "A^": "1", "Av": "1", "^A": "2", "^^": "1", "vA": "1", "vv": "2"}
0 | 379A | {"<<": "1", ">": "1", ">A": "1", "A>": "1", "A^": "2", "Av": "1", "^<": "1", "^A": "1", "^^": "1", "vA": "1", "vv": "2"}
0 | 456A | {"<<": "1", "A": "2", "A>": "2", "A^": "1", "Av": "1", "^<": "1", "^^": "1", "vA": "1", "vv": "1"}
0 | 980A | {"A": "1", "A<": "1", "A>": "1", "A^": "1", "Av": "1", "^A": "1", "^^": "2", "vA": "1", "vv": "2"}
1 | 029A | {"<<": "1", ">": "1", ">A": "3", ">^": "1", "A<": "3", "A>": "2", "AA": "3", "A^": "2", "Av": "2", "^>": "1", "^A": "2", "v<": "1", "v>": "1", "vA": "1"}
1 | 029A | {"<<": "1", ">": "1", ">A": "3", ">^": "1", "A<": "3", "A>": "3", "AA": "3", "A^": "1", "Av": "2", "^>": "1", "^A": "2", "v<": "1", "vA": "2"}
...
SELECT
sum(regexp_replace(src, '\D', '', 'g')::integer * ln)
FROM
(
SELECT DISTINCT ON(src)
src
, (
SELECT
sum(v::bigint) -- суммируем jsonb-значения
FROM
jsonb_each_text(trg) T(k, v)
) ln
FROM
r
WHERE
bot = (SELECT max(bot) FROM r) -- значения с последнего шага
ORDER BY
1, 2
) T;
WITH RECURSIVE src AS (
SELECT
line[1] code
FROM
regexp_matches(
$$
029A
980A
179A
456A
379A
$$
, '[^\r\n]+(?=$|[\r\n])'
, 'g'
) line
)
, numpad AS (
SELECT
d[1] k
, x
, y
FROM
regexp_matches(
$$
789
456
123
0A
$$
, '[^\r\n]+(?=$|[\r\n])'
, 'g'
)
WITH ORDINALITY y(line, y)
, regexp_matches(
line[1]
, '.'
, 'g'
)
WITH ORDINALITY x(d, x)
WHERE
d[1] <> ' '
)
, numpad_step AS (
SELECT DISTINCT
kb1.k || kb2.k pair
, unnest(ARRAY[
repeat(CASE WHEN kb2.x > kb1.x THEN '>' ELSE '<' END, abs(kb2.x - kb1.x)::integer) || repeat(CASE WHEN kb2.y > kb1.y THEN 'v' ELSE '^' END, abs(kb2.y - kb1.y)::integer)
, repeat(CASE WHEN kb2.y > kb1.y THEN 'v' ELSE '^' END, abs(kb2.y - kb1.y)::integer) || repeat(CASE WHEN kb2.x > kb1.x THEN '>' ELSE '<' END, abs(kb2.x - kb1.x)::integer)
]) || 'A' code
FROM
numpad kb1
, numpad kb2
EXCEPT ALL
VALUES
('01', '<^A')
, ('04', '<^^A')
, ('07', '<^^^A')
, ('A1', '<<^A')
, ('A4', '<<^^A')
, ('A7', '<<^^^A')
, ('10', 'v>A')
, ('40', 'vv>A')
, ('70', 'vvv>A')
, ('1A', 'v>>A')
, ('4A', 'vv>>A')
, ('7A', 'vvv>>A')
)
, arrpad AS (
SELECT
d[1] k
, x
, y
FROM
regexp_matches(
$$
^A
$$
, '[^\r\n]+(?=$|[\r\n])'
, 'g'
)
WITH ORDINALITY y(line, y)
, regexp_matches(
line[1]
, '.'
, 'g'
)
WITH ORDINALITY x(d, x)
WHERE
d[1] <> ' '
)
, arrpad_step AS (
SELECT DISTINCT
kb1.k || kb2.k pair
, unnest(ARRAY[
repeat(CASE WHEN kb2.x > kb1.x THEN '>' ELSE '<' END, abs(kb2.x - kb1.x)::integer) || repeat(CASE WHEN kb2.y > kb1.y THEN 'v' ELSE '^' END, abs(kb2.y - kb1.y)::integer)
, repeat(CASE WHEN kb2.y > kb1.y THEN 'v' ELSE '^' END, abs(kb2.y - kb1.y)::integer) || repeat(CASE WHEN kb2.x > kb1.x THEN '>' ELSE '<' END, abs(kb2.x - kb1.x)::integer)
]) || 'A' code
FROM
arrpad kb1
, arrpad kb2
EXCEPT ALL
VALUES
('<^', '^>A')
, ('>A')
, ('^<', '^', '^', '>vA')
, ('Av', 'v^A')
)
, map AS (
SELECT
s.pair src
, jsonb_object(
array_agg(T.t)
, array_agg(T.q)::text[]
) trg
FROM
arrpad_step s
, LATERAL (
SELECT
substr('A' || code, i, 2) t
, count(*) q
FROM
generate_series(1, length(code)) i
GROUP BY
1
) T
GROUP BY
1
)
, r AS (
SELECT
0 bot
, code src
, trg
FROM
src
, LATERAL (
WITH RECURSIVE step AS (
SELECT
i
, substr('A' || code, i, 2) pair
FROM
generate_series(1, length(code)) i
)
, r AS (
SELECT
1 i
, '' code0
UNION ALL
SELECT
i + 1
, code0 || code
FROM
r
JOIN
step
USING(i)
JOIN
numpad_step
USING(pair)
)
TABLE r
ORDER BY
i DESC
FETCH FIRST 1 ROW
WITH TIES
) T0
, LATERAL (
SELECT
jsonb_object(
array_agg(T.t)
, array_agg(T.q)::text[]
) trg
FROM
(
SELECT
substr('A' || code0, i, 2) t
, count(*) q
FROM
generate_series(1, length(code0)) i
GROUP BY
1
) T
) T
UNION ALL
SELECT
bot + 1
, r.src
, T.*
FROM
r
, LATERAL (
SELECT
jsonb_object(
array_agg(k)
, array_agg(v)::text[]
) trg -- собираем счетчики
FROM
(
SELECT
k1 k
, sum(v0::bigint * v1::bigint) v
FROM
jsonb_each_text(r.trg) T0(k0, v0) -- разворачиваем ключ-значение текущего шага
JOIN
map
ON map.src = k0 -- находим, во что превратится каждый ключ
, jsonb_each_text(map.trg) T1(k1, v1) -- разворачиваем ключ-значение следующего шага
GROUP BY
1
) T
) T
WHERE
bot < 25
)
SELECT
sum(regexp_replace(src, '\D', '', 'g')::integer * ln)
FROM
(
SELECT DISTINCT ON(src)
src
, (
SELECT
sum(v::bigint) -- суммируем jsonb-значения
FROM
jsonb_each_text(trg) T(k, v)
) ln
FROM
r
WHERE
bot = (SELECT max(bot) FROM r) -- значения с последнего шага
ORDER BY
1, 2
) T;
