Фильтр Блума

Вспомним про абстрактную структуру данных множество. Для её адекватной реализации в любом случае требуется достаточно много памяти. Мы можем попытаться ослабить это требования за счёт возможности ошибаться.

Мы хотим создать такую структуру данных, в которую можно будет добавлять объекты и пытаться определять наличие в ней объектов. На запрос «есть ли в множестве объект xx?» можно получить или определенный отрицательный ответ «объекта xx точно нет» или неопределенный положительный ответ «объект xx, возможно, в множестве есть».

Пусть все наши объекты — элементы какого-то универсума U\UUU.

Про удаление объектов из нашей структуры пока не думаем и не говорим.

Реализацией нашей идеи служит структура фильтр Блума (Bloom filter). Эта структура данных использует хеширование.

Заведем битовый массив b{0,1}mb \in \{0, 1\}^m, состоящий из mm элементов. Этот битовый массив будет хранить информацию о наличии и отсутствии объектов в нашем множестве.

Создадим еще kk независимых хеш-функций h1,h2,,hk ⁣:U{1,2,,m}h_1, h_2, \dotsc, h_k \colon \UUU \surjto \{1, 2, \dotsb, m\}.

const array h[k] array[bool] b[m]

Добавление элемента

При добавлении элемента xx в множество нужно записать 11 в массив bb на каждую из позиций h1(x),h2(x),,hk(x)h_1(x), h_2(x), \dotsc, h_k(x).

function add(x): for int i = 0; i < k: int index = h[i](x) b[index] = true

Временная сложность выполнения операции — O(k)O(k).

Проверка наличия

Чтобы проверить, что элемент xx принадлежит множеству, нужно проверить, что все биты в массиве bb на позициях h1(x),h2(x),,hk(x)h_1(x), h_2(x), \dotsc, h_k(x) установлены в 11. Если хотя бы один бит равен 00, то элемента xx в множестве быть не может.

function check(x) -> bool: bool contains = true for int i = 0; i < k: int index = h[i](x) contains &= b[index] return contains

Временная сложность выполнения операции — O(k)O(k).

Как уже упоминалось выше, операция check(x) может вернуть или определенный отрицательный ответ, или неопределенный положительный. Из-за этого свойства фильтр Блума нельзя использовать напрямую, если важна точность. Но он очень сильно пригождается тогда, когда надо выполнять много дорогостоящих запросов к другой структуре.

Например, есть у нас какая-то база данных (на самом деле любая структура данных) с дорогим доступом к элементам. Создадим поверх этой базы фильтр Блума, добавив в него все элементы, которые есть в базе данных. Теперь, когда нам приходит запрос к несуществующим данным, мы его отфильтруем фильтром Блума и не пустим дальше к базе данных. В итоге у нас значительно уменьшилось количество дорогостоящих запросов к базе данных ценой небольшого количества дополнительной памяти на фильтр.

Вероятность ошибки и её минимизация

Давайте посчитаем вероятность ложного положительного срабатывания операции check(x). Как и прежде, размер битового массива mm, количество хеш-функций kk. Пусть в нашем множестве уже содержится nn элементов.

Посчитаем вероятность того, что конкретный бит jj не будет установлен в 11 хеш-функцией hih_i при вставке очередного элемента xx.

P(hi(x)j)=11m\prob \big( h_i(x) \neq j \big) = 1 - \frac{1}{m}

Тогда для kk хеш-функций вероятность того, что конкретный бит jj не будет установлен в 11 равна

P(hi(x)ji)=(11m)k\prob \big( h_i(x) \neq j \? \forall\, i \big) = \left( 1 - \frac{1}{m} \right)^k

Теперь можем посчитать вероятность того, что jj-ый бит будет равен 00 после вставки nn элементов в изначально пустой фильтр Блума

P(b[j]=0)=(11m)kn\prob \big( b[j] = 0 \big) = \left( 1 - \frac{1}{m} \right)^{kn}

Ложное положительное срабатывание check(x) происходит тогда, когда для несуществующего элемента xx все kk бит окажутся ненулевыми. Вероятность такого события равна

P(ложное срабатывание)=(1(11m)kn)k\prob (\text{ложное срабатывание}) = \Bigg( 1 - \left( 1 - \frac{1}{m} \right)^{kn} \Bigg)^k

Можно вычислить оптимальное значение kk, которое минимизирует ошибку. В таком виде задача минимизации не решается, придется прибегнуть к асимптотическим оценкам. Используя второй замечательный предел, можно упростить

(11m)knekn/m\left( 1 - \frac{1}{m} \right)^{kn} \approx e^{-kn/m}

Тогда

P(ошибка)(1ekn/m)k\prob (\text{ошибка}) \approx \left( 1 - e^{-kn/m} \right)^k

И вот уже в такой форме можно найти такое kk, при котором вероятность ошибки минимальная. Достаточно прологарифмировать вероятность ошибки и найти ноль производной. В итоге получается

arg minkP(ошибка)mnln2\argmin\limits_k \prob (\text{ошибка}) \approx \frac{m}{n} \cdot \ln 2

Подставив это значение kk можно найти эту самую минимальную вероятность ошибки

minP(ошибка)(11e)m/nln2\min\limits \prob (\text{ошибка}) \approx \left( 1 - \frac{1}{e} \right) ^{m/n \cdot \ln 2}