Манакер: все палиндромы за O(n)
Для каждого центра - радиус наибольшего палиндрома. Тот же приём с самым правым окном, что и в z-функции, только окно теперь палиндромное.
5 мин
Палиндром задаётся центром и радиусом. Центров у строки длины ровно : символов и промежутков между ними.
Алгоритм Манакера для каждого центра находит наибольший палиндром с этим центром. Всё остальное про палиндромы выводится отсюда.
Держат два массива:
- - количество нечётных палиндромов с центром в , оно же радиус вместе с центром. Палиндром занимает ;
- - количество чётных палиндромов с центром между и . Палиндром занимает .
Для abaaba: , . Единственный чётный палиндром длиной больше нуля - вся строка, и её центр лежит между позициями 2 и 3.
Наивно и почему это плохо
Для каждого центра расходиться в обе стороны - . На строке из одинаковых букв это ровно сравнений: при уже .
Ускорение то же, что в z-функции: переиспользовать посчитанное.
Самое правое палиндромное окно
Держим границы - того из уже найденных палиндромов, который кончается правее всех.
Если новый центр попал внутрь, то зеркальная относительно центра окна позиция уже посчитана, и её ответ можно списать. Но только до правой границы: дальше окна мы про строку ничего не знаем.
Дальше - досравнивание вручную и, если палиндром вылез правее , обновление окна.
vector<int> d1(n), d2(n);
for (int i = 0, l = 0, r = -1; i < n; i++) {
int k = (i > r) ? 1 : min(d1[l + r - i], r - i + 1);
while (i - k >= 0 && i + k < n && s[i - k] == s[i + k]) k++;
d1[i] = k--;
if (i + k > r) { l = i - k; r = i + k; }
}
for (int i = 0, l = 0, r = -1; i < n; i++) {
int k = (i > r) ? 0 : min(d2[l + r - i + 1], r - i + 1);
while (i - k - 1 >= 0 && i + k < n && s[i - k - 1] == s[i + k]) k++;
d2[i] = k--;
if (i + k > r) { l = i - k - 1; r = i + k; }
}
Два цикла отличаются тремя мелочами: начальным значением , смещением индекса при списывании () и левой границей при обновлении окна (). Эти три места - главный источник ошибок; их стоит выписать один раз и больше не выводить заново.
Проверено: на 4000 случайных строках длины до 14 над алфавитами из 1-3 букв счёт палиндромных подстрок и наибольший палиндром совпали с полным перебором всех подстрок.
Почему линия
Тот же амортизационный аргумент, что и везде в разделе: каждое успешное сравнение внутри while двигает правую границу вправо хотя бы на единицу, а не убывает и не превосходит . Неуспешное сравнение в каждой итерации ровно одно.
Измерено (оба прохода вместе):
| строка | длина | сравнений | доля от |
|---|---|---|---|
| случайная, алфавит 26 | 1 000 000 | 2 080 088 | 2,08 |
| случайная, алфавит 2 | 1 000 000 | 3 464 024 | 3,46 |
aaaa… |
1 000 000 | 1 999 997 | 2,00 |
abab… |
1 000 000 | 1 999 997 | 2,00 |
| палиндром из 10^6 букв | 1 000 000 | 2 560 568 | 2,56 |
Доля около двух - это и есть «одно неуспешное сравнение на центр, а центров ». Успешных сравнений заметно меньше: они и ограничены движением .
Что из этого следует
Количество палиндромных подстрок - просто сумма: . Каждый палиндром считается один раз, по своему центру.
Наибольшая палиндромная подстрока - максимум по и .
Является ли палиндромом - да, если радиус в центре этого отрезка достаёт до его краёв. Для нечётной длины центр , для чётной - .
Наибольший палиндромный префикс - максимум по палиндромам, у которых левая граница равна нулю; аналогично для суффикса. Впрочем, для одной такой задачи Манакер избыточен: хватает склейки с перевёрнутой строкой.
Манакер или хеши
Палиндромы ищут и хешами: для центра бинарным поиском находят наибольший радиус, сравнивая прямой хеш с обратным. Это и вероятностно.
| Манакер | хеши с бинпоиском | |
|---|---|---|
| время | ||
| ответ | точный | вероятностный |
| длина кода | 12 строк, легко ошибиться | 20 строк, ошибиться труднее |
| обобщается на «почти палиндром» | плохо | хорошо |
Правило: если задача чисто про палиндромы - Манакер. Если палиндромы это лишь часть задачи, где уже нужны хеши, - не заводите второй инструмент.