Алгоритм Манакера - Алгоритмика
Алгоритм Манакера

Алгоритм Манакера

автор Сергей Слотин
опубликовано 2019

Пусть есть строка ss и мы хотим найти в ней все подпалиндромы.

Мы сразу сталкиваемся с очевидной трудностью: их в строке может быть O(n2)O(n^2), что можно видеть на примере строки s=aaas = aa \ldots a. Поэтому будем использовать следующий формат: для каждой позиции sis_i найдём наибольший палиндром, центр которого совпадает с sis_i (чётные и нечётные палиндромы будем рассматривать отдельно). Половину его длины, округлённую вниз, будем называть радиусом.

Наивное решение — перебрать sis_i, а для него вторым циклом находить наибольшую искомую длину:

vector<int> pal_array(string s) {
    int n = s.size();

    // окружим строку спецсимволами, чтобы не рассматривать выход за границы
    s = "#" + s + "$";

    // в этом массиве будем хранить расстояние от центра до границы палиндрома
    vector<int> t(n, 0);

    for(int i = 1; i <= n; i++)
        while (s[i - t[i - 1]] == s[i + t[i - 1]])
            r[i-1]++;

    return r;
}

Тот же пример s=aaas = aa\dots a показывает, что данная реализация работает за O(n2)O(n^2).

Для оптимизации применим идею, знакомую из алгоритма z-функции: при инициализации tit_i будем пользоваться уже посчитанными tt. А именно, будем поддерживать (l,r)(l, r) — интервал, соответствующий самому правому из найденных подпалиндромов. Тогда мы можем сказать, что часть наибольшего палиндрома с центром в sis_i, которая лежит внутри sl:rs_{l:r}, имеет радиус хотя бы min(ri,;tl+ri)\min(r-i, ; t_{l+r-i}). Первая величина равна длине, дальше которой произошел бы выход за пределы sl:rs_{l:r}, а вторая — значению радиуса в позиции, зеркальной относительно центра палиндрома sl:rs_{l:r}.


vector<int> manacher_odd(string s) {
    int n = (int) s.size();
    vector<int> d(n, 1);
    int l = 0, r = 0;
    for (int i = 1; i < n; i++) {
        if (i < r)
            d[i] = min(r - i + 1, d[l + r - i]);
        while (i - d[i] >= 0 && i + d[i] < n && s[i - d[i]] == s[i + d[i]])
            d[i]++;
        if (i + d[i] - 1 > r)
            l = i - d[i] + 1, r = i + d[i] - 1;
    }
    return d;
}

Так же, как и z-функция, алгоритм работает за линейное время: цикл while запускается только когда ti=rit_i = r - i (иначе палиндром уже во что-то упёрся), и каждая его итерация сдвигает увеличивает rr на единицу. Так как rnr \leq n, получаем, что суммарно эти циклы сделают O(n)O(n) итераций.

Для случая чётных палиндромов меняется только индексация:

vector<int> manacher_even(string s) {
    int n = (int) s.size();
    vector<int> d(n, 0);
    int l = -1, r = -1;
    for (int i = 0; i < n - 1; i++) {
        if (i < r)
            d[i] = min(r - i, d[l + r - i - 1]);
        while (i - d[i] >= 0 && i + d[i] + 1 < n && s[i - d[i]] == s[i + d[i] + 1])
            d[i]++;
        if (i + d[i] > r)
            l = i - d[i] + 1, r = i + d[i];
    }
    return d;
}

Также можно было не писать отдельно две реализации, а воспользоваться следующим трюком — сделать замену:

S=s1s2snS=s1#s2##sn S = s_1 s_2 \dots s_n \to S^* = s_1 \# s_2 \# \dots \# s_n Теперь нечётные палиндромы с центром в sis_i соответствуют нечётным палиндромам исходной строки, а нечётные палиндромы с центром в # — чётным.