Нормализация: когда «равны» значит не «равны»
Совпадение с точностью до сдвига, поворота алфавита или перестановки букв. Приём один: свести к инварианту и искать точное равенство.
4 мин
Часто спрашивают не «равны ли куски», а «равны ли они с точностью до чего-нибудь»: до прибавления константы, до поворота алфавита, до перестановки символов.
Приём один и тот же: придумать функцию, которая не меняется при разрешённом преобразовании, применить её к обеим строкам и искать уже точное совпадение. Дальше работают все обычные инструменты - префикс-функция, хеши, бор.
Сдвиг на константу
Даны массивы чисел. Вхождение в «со сдвигом» значит при каком-то общем .
Инвариант - последовательность разностей соседей. Прибавление константы её не меняет:
Значит, надо искать разности в разностях обычной префикс-функцией.
Две ловушки, обе на границах:
Разностей на одну меньше. У массива длины их . При разностей нет вовсе, и подходит любая позиция - этот случай надо обрабатывать отдельно, иначе решение либо упадёт, либо выведет пустоту.
Разности не влезают в тип. При разность лежит в и в int помещается, но если границы чуть шире - уже нет. И сравнивать разности как символы строки нельзя: это числа, нужен вариант префикс-функции по массиву int, а не по char.
Поворот алфавита
Шифр Цезаря - тот же сдвиг, только по модулю размера алфавита. Инвариант тот же: разности соседних букв по модулю 26.
Если вдобавок разрешён циклический сдвиг самой строки, разности надо брать циклически - включая пару «последняя буква, первая». Тогда у строки длины ровно разностей, и циклический сдвиг строки превращается в циклический сдвиг последовательности разностей. А это уже знакомая задача: искать в .
Почему именно циклические разности, а не обычные: при обычных длина падает до , и информация о стыке теряется - строки ab и ba стали бы неразличимы.
Восстановить сам сдвиг просто: если совпадение нашлось в позиции , то первая буква результата - это , и поворот считается из .
Проверено: на 40 000 парах строк длины до 7 ответ совпал с полным перебором всех пар (сдвиг, поворот) - и там, где преобразование существует, и там, где его нет.
Перестановка символов
Анаграммы - это равенство мультимножеств. Инвариантов тут два, и выбор между ними важен.
Счётчики букв. Если алфавит маленький, храните массив из 26 чисел и двигайте окно: при сдвиге на единицу меняются два счётчика. Сравнение окон - , всего . Точно и просто.
Хеш мультимножества. Если значения большие или алфавита нет вовсе, каждому значению сопоставляют случайное 64-битное число и берут сумму по окну. Сумма не зависит от порядка - это и есть коммутативный хеш. Считается префиксными суммами, окно любой длины - за константу.
Подробнее про второй способ - «Хеширование множеств».
Осторожно с соблазном взять вместо случайных чисел сами значения: сумма значений совпадает у и , и такое ломается на первом же тесте. Сумма квадратов ломается чуть позже, но тоже ломается.
Таблица
| «равны с точностью до…» | инвариант | чем искать |
|---|---|---|
| прибавления константы | разности соседей | префикс-функция по массиву |
| поворота алфавита | разности по модулю | префикс-функция |
| циклического сдвига | строка в удвоенной строке | префикс-функция |
| сдвига и поворота вместе | циклические разности | префикс-функция в удвоенной |
| перестановки символов | счётчики или хеш мультимножества | скользящее окно |
| регистра букв | приведение к нижнему регистру | что угодно |
Общее правило: сначала нормализуйте, потом ищите. Попытка «учесть преобразование прямо в алгоритме поиска» почти всегда даёт лишний множитель и лишние ошибки на границах.