Небольшая предыстория. Этот пост я написал для двух целей. Во-первых, обкатать конвертор разметки Markdown +
в хабрачитаемый вид. Во-вторых, рассказать об интересной задаче из data streaming. К концу написания, я обнаружил пост про LogLog четырехлетней давности. На мою удачу автор предыдущего поста делал упор на реализацию. Я же, полагаясь на
, расскажу больше о математике.
Давайте представим, что у нас есть роутер. Через роутер проходит много пакетов по разным адресам. Нам интересно получить статистику, как много адресов задействовано в коммуникации. Есть пара проблем.
![some title](https://habrastorage.org/files/9e5/6e3/758/9e56e3758e6543cf8a7c1b2490bb7c8f)
Задача. Есть последовательность целых чисел
, все числа принимают значения от
до
. Требуется в один проход посчитать количество различных чисел, используя
памяти.
Я расскажу вероятностный приближенный алгоритм Флажолета-Мартина. ТТХ алгоритма:
В конце поста я расскажу, почему точные детерминированные алгоритмы требуют
памяти.
Представим, что у нас есть отрезок действительных чисел
. На отрезок мы независимо случайно кидаем
точек согласно равномерному распределению. Какое будет расстояние между крайней левой точкой и нулем?
Можно предположить, что точки разделят отрезок на
меньших подотрезков примерно одинаковой длины. Если аккуратно записать матожидание расстояния и взять интеграл, мы получим
![](http://tex.s2cms.ru/svg/%0A%5Cmathbf%7BE%7D%5Cleft%5B%5C%3B%5Cmin%28x_1%2C%20%5Ccdots%2C%20x_n%29%20%5C%3B%5Cright%5D%20%3D%20%5Cint_0%5E1%20x%20%5Ccdot%20%5Cleft%28n%20%5Ccdot%20%281%20-%20x%29%5E%7Bn%20-%201%7D%20dx%20%5Cright%29%20%3D%20%5Cfrac%7B1%7D%7Bn%20%2B%201%7D.%0A)
Пусть кто-то кинул случайно несколько точек на отрезок, и
— расстояние от нуля до крайней левой точки. Можно прикинуть, что всего точек порядка
.
Идея алгоритма Флажолета-Мартина — случайно бросить все числа последовательности
на отрезок
, а затем найти расстояние от нуля до крайней левой точки. Если одинаковые числа будут всегда отображаться в одну точку, а разные независимо распределяться по отрезку, мы сможем прикинуть ответ.
Бросать числа на отрезок мы будем с помощью случайной хэш-функции.
Определение. Семейство хэш-функций
называется 2-независимым, если для любых
и ![inline_formula](http://tex.s2cms.ru/svg/%5Cinline%20v_1%2C%20v_2%20%5Cin%20V)
![](http://tex.s2cms.ru/svg/%0A%5Cmathbf%7BPr%7D_%7Bh%20%5Cleftarrow%20U%28%5Cmathcal%7BH%7D%29%7D%5Cleft%5B%5C%3Bh%28k_1%29%20%3D%20v_1%20%5Ctext%7B%20and%20%7D%20h%28k_2%29%20%3D%20v_2%20%5C%3B%5Cright%5D%20%3D%20%5Cfrac%7B1%7D%7B%7CV%7C%5E2%7D.%0A)
Смысл определения в следующем. Зафиксируем два каких угодно ключа
и
.
Ключи — различные. Посмотрим на случайные величины
и
. Случайность задается выбором функции
. Тогда, по определению, величины
и
будут вести себя как независимые.
Как следствие, если взять всего один ключ
, то величина
будет равномерна распределена по
.
Для примера возьмем простое число
. Пусть
.
— это семейство всех линейных отображений по модулю
:
![](http://tex.s2cms.ru/svg/h_%7Ba%2C%20b%7D%28x%29%20%3D%20a%20%5Ccdot%20x%20%2B%20b%20%5Ctext%7B%20mod%20%7D%20p)
для
. Тогда
![](http://tex.s2cms.ru/svg/%0A%5Cbegin%7Barray%7D%7Bl%7D%0A%5Cmathbf%7BPr%7D_%7Bh%7D%5Cleft%5B%5C%3Bh%28k_1%29%20%3D%20v_1%20%5Ctext%7B%20and%20%7D%20h%28k_2%29%20%3D%20v_2%20%5C%3B%5Cright%5D%20%3D%20%5C%5C%20%3D%0A%5Cmathbf%7BPr%7D_%7Ba%2C%20b%7D%5Cleft%5B%5C%3B%5Cleft%5C%7B%5Cbegin%7Barray%7D%7Bl%7Da%20%5Ccdot%20k_1%20%2B%20b%20%3D%20v_1%20%5Ctext%7B%20%28mod%20%7Dp%29%20%5C%5C%20a%20%5Ccdot%20k_2%20%2B%20b%20%3D%20v_2%20%5Ctext%7B%20%28mod%20%7Dp%29%5Cend%7Barray%7D%20%5Cright.%20%5Cright%5D%20%3D%20p%5E%7B-2%7D.%0A%5Cend%7Barray%7D%0A)
Поскольку
, система имеет ровно одно решение среди
возможных:
![](http://tex.s2cms.ru/svg/%0A%5Cbegin%7Barray%7D%7Bl%7D%0Aa%20%3D%20%28v_1%20-%20v_2%29%20%5Ccdot%20%28k_1%20-%20k_2%29%5E%7Bp%20-%202%7D%20%5Ctext%7B%20mod%20%7Dp%2C%5C%5C%0Ab%20%3D%20%28k_1%20%5Ccdot%20v_2%20-%20k_2%20%5Ccdot%20v_1%29%20%5Ccdot%20%28k_1%20-%20k_2%29%5E%7Bp%20-%202%7D%20%5Ctext%7B%20mod%20%7D%20p.%0A%5Cend%7Barray%7D%0A)
Поймем два важных момента. Во-первых, хранение такой функции обходится в
памяти, что очень немного. Во-вторых, если внимательно приглядеться, то можно понять, что вычисления проходят в поле
, и могут быть обобщены для любого конечного поля.
В тестовом коде я буду использовать поле Галуа
. В описании далее можно считать, что у нас есть семейство хэш-функций
, где
— степень двойки. Хранение одной функции занимает
памяти.
Пусть
— степень двойки.
Перед стартом, алгоритм случайно выбирает хэш-функцию
из 2-независимого семейства.
Будем бросать элементы последовательности на отрезок
. Берем очередное значение
и записываем: ноль, точка,
в двоичном виде. Например, если
, то получится число
.
Обозначим через
число лидирующих нулей в двоичной записи
. Пусть
. Мы знаем, что минимальное значение лежит между
и
.
Ответ алгоритма:
.
Я планирую показать, что ответ алгоритма будет в 3 раза больше верного с вероятностью меньшей
. Аналогично, алгоритм вернет ответ в 3 раза меньше верного вероятностью меньшей
. Если вы не планируете углубляться в математику, смело переходите к следующей части.
Обозначим через
множество всех различных чисел последовательности
, а
— их количество.
Для анализа алгоритма, нам потребуются два набора случайных величин.
Заметим, что вероятность
: величина
равномерно распределена по отрезку
;
— степень двойки; есть всего
чисел с
лидирующими нулями.
Значит, матожидание
. Ограничим сверху дисперсию
![](http://tex.s2cms.ru/svg/%0A%5Cmathbf%7BD%7D%5Cleft%5B%5C%3BX_%7Bj%2C%20r%7D%5C%3B%5Cright%5D%20%3D%20%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX%5E2_%7Bj%2C%20r%7D%5C%3B%5Cright%5D%20-%20%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX_%7Bj%2C%20r%7D%5C%3B%5Cright%5D%5E2%20%5Cleq%20%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX%5E2_%7Bj%2C%20r%7D%5C%3B%5Cright%5D%20%3D%20%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX_%7Bj%2C%20r%7D%5C%3B%5Cright%5D%20%3D%202%5E%7B-r%7D.%0A)
Заметим, что дисперсия по величинам
линейна. Для любых двух
и ![inline_formula](http://tex.s2cms.ru/svg/%5Cinline%20j)
![](http://tex.s2cms.ru/svg/%0A%5Cmathbf%7BD%7D%5Cleft%5B%5C%3BX_%7Bi%2C%20r%7D%20%2B%20X_%7Bj%2C%20r%7D%5C%3B%5Cright%5D%20%3D%20%5Cmathbf%7BE%7D%5Cleft%5B%5C%3B%28X_%7Bi%2C%20r%7D%20%2B%20X_%7Bj%2C%20r%7D%29%5E2%5C%3B%5Cright%5D%20-%20%5Cleft%28%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX_%7Bi%2C%20r%7D%5C%3B%5Cright%5D%20%2B%20%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX_%7Bj%2C%20r%7D%5C%3B%5Cright%5D%5Cright%29%5E2%0A)
![](http://tex.s2cms.ru/svg/%0A%5Cmathbf%7BE%7D%5Cleft%5B%5C%3B%28X_%7Bi%2C%20r%7D%20%2B%20X_%7Bj%2C%20r%7D%29%5E2%5C%3B%5Cright%5D%20%3D%20%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX_%7Bi%2C%20r%7D%5E2%5C%3B%5Cright%5D%20%2B%202%20%5Ccdot%20%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX_%7Bi%2C%20r%7D%20%5Ccdot%20X_%7Bj%2C%20r%7D%5C%3B%5Cright%5D%20%2B%20%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX_%7Bj%2C%20r%7D%5E2%5C%3B%5Cright%5D%20%0A)
![](http://tex.s2cms.ru/svg/%0A%5Cleft%28%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX_%7Bi%2C%20r%7D%5C%3B%5Cright%5D%20%2B%20%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX_%7Bj%2C%20r%7D%5C%3B%5Cright%5D%5Cright%29%5E2%20%3D%20%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX_%7Bi%2C%20r%7D%5C%3B%5Cright%5D%5E2%20%2B%202%20%5Ccdot%20%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX_%7Bi%2C%20r%7D%5C%3B%5Cright%5D%20%5Ccdot%20%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX_%7Bj%2C%20r%7D%5C%3B%5Cright%5D%20%2B%20%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX_%7Bj%2C%20r%7D%5C%3B%5Cright%5D%5E2%20%0A)
Поскольку
и
независимы, то
. Значит,
![](http://tex.s2cms.ru/svg/%0A%5Cmathbf%7BD%7D%5Cleft%5B%5C%3BX_%7Bi%2C%20r%7D%20%2B%20X_%7Bj%2C%20r%7D%5C%3B%5Cright%5D%20%3D%20%28%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX_%7Bi%2C%20r%7D%5E2%5C%3B%5Cright%5D%20-%20%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX_%7Bi%2C%20r%7D%5C%3B%5Cright%5D%5E2%29%20%2B%20%28%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX_%7Bj%2C%20r%7D%5E2%5C%3B%5Cright%5D%20-%20%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BX_%7Bj%2C%20r%7D%5C%3B%5Cright%5D%5E2%29%20%3D%20%5C%5C%20%3D%20%20%5Cmathbf%7BD%7D%5Cleft%5B%5C%3BX_%7Bi%2C%20r%7D%20%5C%3B%5Cright%5D%20%2B%20%20%5Cmathbf%7BD%7D%5Cleft%5B%5C%3BX_%7Bj%2C%20r%7D%20%5C%3B%5Cright%5D.%0A)
Более того,
, поскольку величины
— 2-независимы.
Теперь рассмотрим величину
.
Пусть
— минимальное число такое, что
. Событие «алгоритм выдал ответ в 3 раза больше нужного» равносильно событию
и равносильно событию
. Применяя неравенство Маркова, ограничим вероятность
![](http://tex.s2cms.ru/svg/%0A%5Cmathbf%7BPr%7D%5Cleft%5B%5C%3BFM%28%5Csigma%29%20%5Cgeq%203%20%5Ccdot%20d%20%5C%3B%5Cright%5D%20%3D%20%5Cmathbf%7BPr%7D%5Cleft%5B%5C%3Bz%20%5Cgeq%20a%5C%3B%5Cright%5D%20%3D%20%5Cmathbf%7BPr%7D%5Cleft%5B%5C%3BY_%7Ba%7D%20%3E%200%5C%3B%5Cright%5D%20%3D%20%5Cmathbf%7BPr%7D%5Cleft%5B%5C%3BY_%7Ba%7D%20%5Cgeq%201%5C%3B%5Cright%5D%20%5Cleq%20%5Cmathbf%7BE%7D%5Cleft%5B%5C%3BY_a%5C%3B%5Cright%5D%5C%3B%2F%5C%3B1%20%3D%20d%20%5Ccdot%202%5E%7B-a%7D%20%5Cleq%5Cfrac%7B%5Csqrt%7B2%7D%7D%7B3%7D%20%5Cleq%200.48.%0A)
Пусть
— максимальное число такое, что
. Аналогично, событие «алгоритм выдал ответ в 3 раза меньше нужного» равносильно событию
и равносильно событию
. Применяя неравенство Чебышёва, получим
![](http://tex.s2cms.ru/svg/%0A%5Cbegin%7Barray%7D%7Bl%7D%0A%5Cmathbf%7BPr%7D%5Cleft%5B%5C%3BFM%28%5Csigma%29%20%5Cleq%20d%5C%3B%2F%5C%3B3%20%5C%3B%5Cright%5D%20%3D%20%5Cmathbf%7BPr%7D%5Cleft%5B%5C%3Bz%20%5Cleq%20b%5C%3B%5Cright%5D%20%3D%20%5Cmathbf%7BPr%7D%5Cleft%5B%5C%3BY_%7Bb%20%2B%201%7D%20%3D%200%5C%3B%5Cright%5D%20%3D%20%5C%5C%20%5C%5C%20%3D%5Cmathbf%7BPr%7D%5Cleft%5B%5C%3B%5Cleft%7CY_%7Bb%20%2B%201%7D%20-%20%5Cfrac%7Bd%7D%7B2%5E%7Bb%2B1%7D%7D%5Cright%7C%20%5Cgeq%20%20%5Cfrac%7Bd%7D%7B2%5E%7Bb%2B1%7D%7D%5C%3B%5Cright%5D%20%5Cleq%20%5Cfrac%7B%5Cmathbf%7BD%7D%5Cleft%5B%5C%3BY_%7Bb%20%2B1%7D%5C%3B%5Cright%5D%7D%7B%28d%20%5C%3B%2F%5C%3B2%5E%7Bb%20%2B%201%7D%29%5E2%7D%20%3D%20%5Cfrac%7B2%5E%7Bb%20%2B%201%7D%7D%7Bd%7D%20%5Cleq%20%5Cfrac%7B%5Csqrt%7B2%7D%7D%7B3%7D%20%5Cleq%200.48.%0A%5Cend%7Barray%7D%0A)
Осталось понять как понизить ошибку. Возьмем случай, когда алгоритм выдает слишком большой ответ. Запустим алгоритм параллельно
раз и вернем медиану среди ответов. Я утверждаю, что если
, то алгоритм ошибется с вероятностью не больше
. Аналогично, ограничив ошибку в другую сторону, получим
![](http://tex.s2cms.ru/svg/%0A%0A%5Cmathbf%7BPr%7D%5Cleft%5B%5C%3BFM_k%28%5Csigma%29%20%5Cleq%20d%5C%3B%2F%5C%3B3%20%5Cvee%20FM_k%28%5Csigma%29%20%5Cgeq%203%20%5Ccdot%20d%5C%3B%5Cright%5D%20%5Cleq%20%5Cmathbf%7BPr%7D%5Cleft%5B%5C%3BFM_k%28%5Csigma%29%20%5Cleq%20d%5C%3B%2F%5C%3B3%20%5Cright%5D%20%2B%20%5Cmathbf%7BPr%7D%5Cleft%5B%5C%3BFM_k%28%5Csigma%29%20%5Cgeq%203%20%5Ccdot%20d%20%5Cright%5D%20%5Cleq%20%5Cdelta.%0A%0A)
Почему медиана так работает? По неравенству Чернова. Заведем случайную величину
. Величина
равна единице, если ответ алгоритма на
запуске меньше
. Вероятность этого события не меньше 0.52.
Если медиана
запусков алгоритма больше
, то это значит, что алгоритм хотя бы половину раз выдал ответ больший
и
. Тогда по неравенству Хефдинга-Чернова
![](http://tex.s2cms.ru/svg/%0A%5Cmathbf%7BPr%7D%5Cleft%5B%5C%3B%5Csum_i%20Z_i%20%5Cleq%20k%5C%3B%2F%5C%3B2%20%5C%3B%5Cright%5D%20%5Cleq%20%5Cmathbf%7BPr%7D%5Cleft%5B%5C%3B%5Cleft%7C%200.52%20%5Ccdot%20k%20-%20%5Csum_i%20Z_i%20%5Cright%7C%20%5Cgeq%200.02%20%5Ccdot%20k%20%5C%3B%5Cright%5D%20%5Cleq%202%5E%7B-c%20%5Ccdot%20k%7D%2C%0A)
где
— некоторая константа. Другой случай показывается аналогично.
Давайте представим, что кто-то действительно придумал детерминированный алгоритм, который в один проход находит точное число различных элементов в один проход. Мы покажем, что такой алгоритм должен использовать
памяти.
Возьмем множество
размера
и положим его в качестве начала последовательности. Скормим эту часть алгоритму и посмотрим на его память.
Из одной только памяти алгоритма можно извлечь все множество
. Если скормить в текущем состоянии число
, ответ алгоритма не изменится; если
, то увеличится на 1. Значит, каждому множеству
должно соответствовать свое уникальное состояние памяти.
Различных подмножеств из
размера
примерно
. Если мы хотим каждому множеству сопоставить битовую строку, нам потребуется ![inline_formula](http://tex.s2cms.ru/svg/%5Cinline%20%5COmega%28%5Clog%20%282%5En%20%5C%2C%2F%5C%2Cn%29%29%20%3D%20%5COmega%28n%29)
Давайте представим, что у нас есть роутер. Через роутер проходит много пакетов по разным адресам. Нам интересно получить статистику, как много адресов задействовано в коммуникации. Есть пара проблем.
- Пакетов так много, что запомнить их все нельзя. Сказать ушедшему пакету «Вернись! Я все прощу,» — тоже.
- Всех возможных адресов
. Столько памяти на роутере нет.
Задача. Есть последовательность целых чисел
Я расскажу вероятностный приближенный алгоритм Флажолета-Мартина. ТТХ алгоритма:
- использует
памяти!
- работает на любом входе;
- находит ответ, который отличается от точного не более чем в 3 раза с вероятностью
:
вероятность берется по случайным битам алгоритма.
В конце поста я расскажу, почему точные детерминированные алгоритмы требуют
Алгоритм Флажолета-Мартина
Представим, что у нас есть отрезок действительных чисел
Можно предположить, что точки разделят отрезок на
Пусть кто-то кинул случайно несколько точек на отрезок, и
Идея алгоритма Флажолета-Мартина — случайно бросить все числа последовательности
2-независимые хэш-функции
Бросать числа на отрезок мы будем с помощью случайной хэш-функции.
Определение. Семейство хэш-функций
Смысл определения в следующем. Зафиксируем два каких угодно ключа
Ключи — различные. Посмотрим на случайные величины
Как следствие, если взять всего один ключ
Для примера возьмем простое число
для
Поскольку
Поймем два важных момента. Во-первых, хранение такой функции обходится в
В тестовом коде я буду использовать поле Галуа
Алгоритм
Пусть
Перед стартом, алгоритм случайно выбирает хэш-функцию
Будем бросать элементы последовательности на отрезок
Обозначим через
Ответ алгоритма:
def init():
h = H.sample()
z = 0
def process(a):
z = max(z, zero(h(a))
def answer():
return 2**(z + 0.5)
Анализ
Я планирую показать, что ответ алгоритма будет в 3 раза больше верного с вероятностью меньшей
Обозначим через
Для анализа алгоритма, нам потребуются два набора случайных величин.
. Принимает значение 1, если количество лидирующих нулей в двоичной записи
хотя бы
; иначе —
.
. Величина
больше нуля, если переменная
по завершении алгоритма оказалась не меньше
.
Заметим, что вероятность
Значит, матожидание
Заметим, что дисперсия по величинам
Поскольку
Более того,
Теперь рассмотрим величину
по линейности матожидания.
по линейности дисперсии для 2-независимых величин.
Пусть
Пусть
Финальный аккорд: медиана
Осталось понять как понизить ошибку. Возьмем случай, когда алгоритм выдает слишком большой ответ. Запустим алгоритм параллельно
Почему медиана так работает? По неравенству Чернова. Заведем случайную величину
Если медиана
где
Нижняя оценка для точного алгоритма
Давайте представим, что кто-то действительно придумал детерминированный алгоритм, который в один проход находит точное число различных элементов в один проход. Мы покажем, что такой алгоритм должен использовать
Возьмем множество
Из одной только памяти алгоритма можно извлечь все множество
Различных подмножеств из