В этой челлендж-серии статей, начатой, внезапно, с разбора задачи Day 11, попробуем использовать PostgreSQL как среду для решения задач Advent of Code 2024.
Возможно, SQL не самый подходящий для этого язык, зато мы рассмотрим его различные возможности, о которых вы могли и не подозревать.
Оригинальная постановка задачи и ее перевод:
Advent of Code 2024, Day 1: Historian Hysteria
--- День 1: Истерия историка ---
...
В офисе Шефа исторически значимые места перечислены не по названию, а по уникальному номеру, который называется идентификатором местоположения. Чтобы убедиться, что они ничего не упустят, Историки разделились на две группы, каждая из которых обыскивает офис и пытается создать свой собственный полный список идентификаторов местоположений.
Есть только одна проблема: если держать два списка рядом (ваш пазл), то быстро становится ясно, что списки не очень похожи. Может быть, вы можете помочь Историкам согласовать их списки?
Например:
3 4
4 3
2 5
1 3
3 9
3 3
Может быть, списки отличаются лишь на небольшую величину! Чтобы узнать, соедините числа и измерьте, насколько они далеки друг от друга. Соедините наименьшее число в левом списке с наименьшим числом в правом списке, затем второе наименьшее число слева со вторым наименьшим числом справа и так далее.
В каждой паре выясните, насколько далеко друг от друга находятся два числа; вам нужно будет сложить все эти расстояния. Например, если вы соедините 3
из левого списка с 7
из правого списка, расстояние между ними будет 4
; если вы соедините 9
с 3
, расстояние между ними будет 6
.
В приведенном выше примере списка пары и расстояния будут следующими:
Наименьшее число в левом списке —
1
, а наименьшее число в правом списке —3
. Расстояние между ними —2
.Второе наименьшее число в левом списке —
2
, а второе наименьшее число в правом списке — еще одно3
. Расстояние между ними —1
.Третье наименьшее число в обоих списках —
3
, поэтому расстояние между ними —0
.Следующие числа, которые нужно объединить в пары, — это
3
и4
, на расстоянии1
.Пятыми по величине числами в каждом списке являются
3
и5
, расстояние между которыми равно2
.Наконец, наибольшее число в левом списке —
4
, а наибольшее число в правом списке —9
; они находятся на расстоянии5
друг от друга.
Чтобы найти общее расстояние между левым списком и правым списком, сложите расстояния между всеми найденными вами парами. В примере выше это , 2 + 1 + 0 + 1 + 2 + 5
общее расстояние 11
!
Ваши фактические левые и правые списки содержат много идентификаторов местоположений. Каково общее расстояние между вашими списками?
--- Часть вторая ---
Ваш анализ только подтвердил то, чего все опасались: два списка идентификаторов местоположений действительно сильно различаются.
Или нет?
Историки не могут прийти к единому мнению о том, какая группа допустила ошибки или как читать большую часть почерка Шефа, но в суматохе вы замечаете интересную деталь: в обоих списках появляется много идентификаторов местоположений! Возможно, другие числа — это вовсе не идентификаторы местоположений, а неверно истолкованный почерк.
На этот раз вам нужно будет выяснить, как часто каждое число из левого списка появляется в правом списке. Рассчитайте общую оценку сходства, сложив каждое число в левом списке после умножения его на количество раз, когда это число появляется в правом списке.
Вот те же примеры списков еще раз:
3 4
4 3
2 5
1 3
3 9
3 3
Для этих списков-примеров процесс нахождения показателя схожести выглядит следующим образом:
Первое число в левом списке —
3
. Оно появляется в правом списке три раза, поэтому показатель сходства увеличивается на .3 * 3 = 9
Второе число в левом списке —
4
. Оно появляется в правом списке один раз, поэтому показатель схожести увеличивается на .4 * 1 = 4
Третье число в левом списке —
2
. Оно не отображается в правом списке, поэтому показатель сходства не увеличивается (2 * 0 = 0
).Четвертый номер,
1
, также не отображается в правом списке.Пятое число,
3
, появляется в правом списке три раза; показатель сходства увеличивается на9
.Последнее число,
3
, появляется в правом списке три раза; показатель сходства снова увеличивается на9
.
Итак, для этих списков-примеров оценка сходства в конце этого процесса составляет 31
( 9 + 4 + 0 + 0 + 9 + 9
).
Еще раз рассмотрите ваши левый и правый списки. Какова их степень сходства?
Часть 1
Для начала, нам стоит преобразовать входящий текст в пары чисел, стоящих на каждой строке - для этого отлично подойдет функция
regexp_matches
.Преобразуем полученные на каждой строке массивы из пары текстовых значений в пару числовых через преобразование типов
::numeric[]
."Соберем"
array_agg(... ORDER BY ...)
упорядоченные массивы для каждого из списков:"Развернем" с помощью "параллельного"
unnest
оба массива одновременно и просуммируем модуль разности значений:
SELECT
sum(abs(v2 - v1)) -- суммируем разницу значений
FROM
(
SELECT
array_agg(line[1] ORDER BY line[1]) lst1 -- упорядочиваем элементы каждого списка
, array_agg(line[2] ORDER BY line[2]) lst2
FROM
(
SELECT
line::numeric[] -- переводим строковые представления в числовые значения
FROM
regexp_matches($$
3 4
4 3
2 5
1 3
3 9
3 3
$$
, '(\d+)\s+(\d+)' -- каждая пара чисел, невзирая на переводы строк
, 'g'
) line
) T
) T
, unnest(lst1, lst2) u(v1, v2); -- "разворачиваем" оба массива параллельно
На входном наборе из задачи ответ получается примерно за 5.5мс:
Часть 2
Воспользуемся наработками из первой части, но заметим, что описанный алгоритм подсчета "схожести" сводится к "каждое из уникальных числовых значений в списках умножь на количество его появлений в первом и втором списках". Для этого:
С помощью
unnest(ARRAY[...])
"размножим" строки, получив для каждого из чисел в списках по строке с признакомlst
, к какому из списков оно относится.Вычислим метрику для каждого значения, используя подсчет с условием
count(*) FILTER(WHERE lst = ...)
.Останется лишь просуммировать полученные значения.
SELECT
sum(sym) -- суммарная "схожесть"
FROM
(
SELECT
v * count(*) FILTER(WHERE lst = 0) * count(*) FILTER(WHERE lst = 1) sym -- "схожесть" для конкретного значения
FROM
(
SELECT
array_agg(line[1] ORDER BY line[1]) lst1
, array_agg(line[2] ORDER BY line[2]) lst2
FROM
(
SELECT
line::numeric[]
FROM
regexp_matches($$
3 4
4 3
2 5
1 3
3 9
3 3
$$
, '(\d+)\s+(\d+)'
, 'g'
) line
) T
) T
, unnest(lst1, lst2) u(v1, v2)
, unnest(ARRAY[v1, v2], ARRAY[0, 1]) g(v, lst) -- "размножаем" строки
GROUP BY
v
) T;
В этот раз вычисления заняли чуть больше времени - порядка 8мс.