Полиномиальное хеширование
Строка как число в системе счисления с основанием p. Схема Горнера, выбор параметров и одна деталь, без которой всё ломается.
3 мин
Основной способ хешировать строки. Идея: смотреть на строку как на запись числа в системе счисления с основанием .
Число в десятичной записи — это . Сделаем то же самое со строкой:
где — числовой код -го символа.
Без взятия по модулю такая запись однозначна: разным строкам соответствуют разные числа, как разным записям соответствуют разные числа в десятичной системе. Модуль сжимает бесконечное множество строк в конечный диапазон, и коллизии появляются — но уже не тривиальные.
Выбор параметров
Основание должно быть строго больше любого кода символа. Иначе система счисления «переполняется» и запись перестаёт быть однозначной: например, при строки c (код 3) и aa (коды 1, 1) обе дают .
Для строчных латинских букв берут что-нибудь от до : 31, 131, 239, 257, 667. Конкретное значение не принципиально.
Модуль берут большим простым: , , . Простота не обязательна теоретически, но избавляет от целого класса проблем и ничего не стоит.
Коды символов: начинайте с единицы
Самая коварная деталь всего раздела.
Естественно написать s[i] - 'a', чтобы a стало нулём. Так делать нельзя: тогда все строки, состоящие из одних a, получат хеш — и a, и aa, и aaa.
Это ровно тот «очевидный контрпример», которого быть не должно, и он найдётся на любом наборе тестов.
Правильно — s[i] - 'a' + 1, чтобы коды шли с единицы. Аналог: в записи числа не бывает ведущих нулей.
Вычисление: схема Горнера
Формулу выше не считают «в лоб» — возводить в степень для каждого символа незачем. Перепишем её со скобками:
Отсюда цикл:
long long h = 0;
for (char c : s) {
h = mult(h, P);
h = add(h, c - 'a' + 1);
}
Линия, одно умножение и одно сложение на символ. Функции add и mult — быстрая модульная арифметика; писать % напрямую не стоит.
По модулю нужно брать на каждом шаге. Накопить сумму и взять модуль в конце нельзя: long long переполнится, а остаток от переполненного значения — не тот остаток.
Проверка
На всех строках длины от 1 до 8 над алфавитом из трёх букв полиномиальный хеш с , даёт 9840 различных значений — ни одной коллизии.
Это не доказательство, но хороший признак: перебором маленьких строк контрпример не находится.
Отладка
Хеши — это большие бессмысленные числа, и смотреть на них глазами обычно бесполезно. Есть приём, который делает их читаемыми.
Возьмите на время отладки:
- алфавит из первых девяти букв,
a…i, чтобы коды были от 1 до 9; - основание ;
- короткие строки, чтобы модуль не срабатывал.
Тогда строка abcea получит хеш — то есть буквально свою запись цифрами. Любая ошибка в коде становится видна сразу: вместо ожидаемых цифр в хеше появляется что-то другое.
Приём стоит помнить: он превращает неотлаживаемую задачу в отлаживаемую за две минуты.
Что дальше
Пока мы умеем считать хеш строки целиком. Настоящая польза начинается, когда нужно сравнивать подстроки одной строки, — и для этого достаточно один раз посчитать префиксные хеши.