Коллизии и парадокс дней рождения
Почему модуля 10^9 хватает для ста тысяч сравнений и не хватает для миллиона строк. Как считать нужный размер хеша.
3 мин
Вероятность того, что два конкретных различных объекта получат одинаковый хеш, — примерно .
Отсюда легко сделать неверный вывод, что модуля хватит почти всегда. Это не так, и разница может стоить решения.
Парадокс дней рождения
Классический факт: в группе из 23 человек вероятность того, что у каких-то двоих совпадают дни рождения, уже больше . При 365 возможных днях это кажется невероятным.
Объяснение в том, что сравниваются не человек с фиксированной датой, а все пары. При 23 людях пар , и каждая совпадает с вероятностью .
Общее правило: среди случайных значений из возможных совпадение появляется, когда достигает примерно .
Что это значит для хешей
Задача: даётся строк, для каждой сказать, встречалась ли она раньше.
Естественное решение — складывать хеши в set. Но проверка «встречался ли хеш» — это неявное сравнение с каждой предыдущей строкой. Всего таких сравнений .
При ожидаемое число коллизий — около . То есть коллизия не «маловероятна», а гарантирована, и решение выдаст неверный ответ.
По правилу корня: . Как только различных объектов становится сильно больше тридцати тысяч, модуля не хватает.
Как считать нужный модуль
Пусть — число объектов, которые попадают в общее хранилище (множество, словарь), и все они сравниваются друг с другом неявно.
Хотим, чтобы это было сильно меньше единицы, значит должно быть заметно больше .
| сколько объектов | нужный модуль |
|---|---|
| с большим запасом | |
| — на грани, лучше больше | |
| на грани |
Если же сравниваются только заданные пары — например, запросов «равны ли строки и », — оценка другая: коллизий ожидается , и при , вероятность ошибки . Здесь модуля хватает.
Ключевой вопрос: сколько пар реально сравнивается. Ответ на него и определяет размер хеша.
Как получить большой модуль
Один хеш по модулю . Требует __int128 для умножения. Работает, но медленнее.
Два хеша по модулям около . Объекты считаются равными, только если совпали оба. Вероятность этого — произведение вероятностей, то есть примерно .
Второй вариант обычно предпочтительнее: все операции остаются в long long, а замедление — ровно вдвое.
struct DoubleHasher {
Hasher h1, h2; // разные p и разные mod
pair<long long, long long> get(int l, int r) const {
return {h1.get(l, r), h2.get(l, r)};
}
};
Пара хешей кладётся в set<pair<long long, long long>> или упаковывается в одно 64-битное число как h1 * MOD2 + h2.
Осторожно с оптимизмом
Оценка предполагает, что хеши распределены равномерно и независимо от входных данных. Если противник знает ваши и , он может подобрать тест, где коллизия гарантирована при любом модуле, — и это делают.
Поэтому на площадках со взломами к оценке добавляют случайность: выбирают случайно во время выполнения.