SQL HowTo: регулярные выражения и условная агрегация (Advent of Code 2024, Day 1: Historian Hysteria)

от автора

В этой челлендж-серии статей, начатой, внезапно, с разбора задачи 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.

Итак, для этих списков-примеров оценка сходства в конце этого процесса составляет 319 + 4 + 0 + 0 + 9 + 9).

Еще раз рассмотрите ваши левый и правый списки. Какова их степень сходства?

Часть 1

  1. Для начала, нам стоит преобразовать входящий текст в пары чисел, стоящих на каждой строке — для этого отлично подойдет функция regexp_matches.

  2. Преобразуем полученные на каждой строке массивы из пары текстовых значений в пару числовых через преобразование типов ::numeric[].

  3. «Соберем» array_agg(... ORDER BY ...) упорядоченные массивы для каждого из списков:

  4. «Развернем» с помощью «параллельного» 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

Воспользуемся наработками из первой части, но заметим, что описанный алгоритм подсчета «схожести» сводится к «каждое из уникальных числовых значений в списках умножь на количество его появлений в первом и втором списках». Для этого:

  1. С помощью unnest(ARRAY[...]) «размножим» строки, получив для каждого из чисел в списках по строке с признаком lst, к какому из списков оно относится.

  2. Вычислим метрику для каждого значения, используя подсчет с условием count(*) FILTER(WHERE lst = ...).

  3. Останется лишь просуммировать полученные значения.

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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *