В этой челлендж-серии статей, начатой, внезапно, с разбора задачи Day 11, попробуем использовать PostgreSQL как среду для решения задач Advent of Code 2024.
Возможно, SQL не самый подходящий для этого язык, зато мы рассмотрим его различные возможности, о которых вы могли и не подозревать.
-
Решение Day 1: Historian Hysteria (регулярные выражения и условная агрегация)
-
Решение Day 5: Print Queue (поиск в словаре и массивах, сортировка «пузырьком»)
-
Решение Day 11: Plutonian Pebbles (агрегация внутри рекурсии)
Оригинальная постановка задачи и ее перевод:
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мс.
ссылка на оригинал статьи https://habr.com/ru/articles/868964/
Добавить комментарий