Manacher

Description

Given a string ss of length nn, find all pairs (i,j)(i, j) such that the substring s[i…j]s[i \dots j] is a palindrome. A string tt is a palindrome when t=trevt = t_{\text{rev}} (trevt_{\text{rev}} is the reverse of tt).

Explanation

There may be O(n2)O(n^2) palindromic substrings in the worst case, so at first glance it seems that this problem has no linear-time algorithm.

However, information about palindromic substrings can be expressed in a more compact way: for every position i=0…n−1i = 0 \dots n - 1, we determine the values d1[i]d_1[i] and d2[i]d_2[i]. They represent, respectively, the number of odd-length and even-length palindromic substrings centered at position ii. Equivalently, they represent the radius of the longest palindrome centered at position ii (the radius lengths d1[i]d_1[i] and d2[i]d_2[i] are both the number of characters from position ii through the rightmost position of the palindrome).

For example, the string s=abababcs = \mathtt{abababc} has three odd-length palindromic substrings centered at s[3]=bs[3] = b. The longest palindrome has radius 33, so d1[3]=3d_1[3] = 3:

a b a bs3 a b⏞d1[3]=3 ca\ \overbrace{b\ a\ \underset{s_3}{b}\ a\ b}^{d_1[3]=3}\ c

The string s=cbaabds = \mathtt{cbaabd} has two even-length palindromic substrings centered at s[3]=as[3] = a. The longest palindrome has radius 22, so d2[3]=2d_2[3] = 2:

c b a as3 b⏞d2[3]=2 dc\ \overbrace{b\ a\ \underset{s_3}{a}\ b}^{d_2[3]=2}\ d

The key idea is that if we have a palindrome of length ll centered at some position ii, then we also have palindromes of lengths l−2l - 2, l−4l - 4, and so on centered at ii. Thus, the two arrays d1[i]d_1[i] and d2[i]d_2[i] contain enough information about all palindromic substrings of the string.

A surprising fact is that there is a sufficiently simple linear-time algorithm for computing the two arrays of palindromic properties, d1[]d_1[] and d2[]d_2[]. In this article, we describe the algorithm in detail.

Solution

In general, there are several ways to solve this problem: using string hashing, it can be solved in O(nlog⁡n)O(n \log n) time, while using a suffix array and fast LCA, it can be solved in O(n)O(n) time.

However, the algorithm described here is overwhelmingly simple and has smaller constants in both time and space complexity. The algorithm was proposed by Glenn K. Manacher in 1975.

Naive algorithm

To avoid ambiguity in what follows, let us clarify what we mean by the “naive algorithm.”

The algorithm works as follows: for each center position ii, after comparing a pair of corresponding characters, it tries to increase the answer by 11 whenever possible.

This algorithm is rather slow: it can compute the answer only in O(n2)O(n^2) time.

The naive algorithm is implemented as follows:

Implementation

C++

vector<int> d1(n), d2(n);
for (int i = 0; i < n; i++) {
  d1[i] = 1;
  while (0 <= i - d1[i] && i + d1[i] < n && s[i - d1[i]] == s[i + d1[i]]) {
    d1[i]++;
  }

  d2[i] = 0;
  while (0 <= i - d2[i] - 1 && i + d2[i] < n &&
         s[i - d2[i] - 1] == s[i + d2[i]]) {
    d2[i]++;
  }
}

Python

d1 = [0] * n
d2 = [0] * n
for i in range(0, n):
    d1[i] = 1
    while 0 <= i - d1[i] and i + d1[i] < n and s[i - d1[i]] == s[i + d1[i]]:
        d1[i] += 1

    d2[i] = 0
    while 0 <= i - d2[i] - 1 and i + d2[i] < n and s[i - d2[i] - 1] == s[i + d2[i]]:
        d2[i] += 1

Manacher's algorithm

Here we describe only the case of finding all odd-length palindromic substrings, that is, computing only d1[]d_1[]; the algorithm for finding all even-length palindromic substrings (that is, computing d2[]d_2[]) requires only a few small modifications to the odd-length case.

To compute the values quickly, we maintain the boundaries [l,r][l, r] of the rightmost palindromic substring found (that is, the palindrome with the maximum value of rr, where ll and rr are the positions of its left and right boundaries, respectively). Initially, we set l=0l = 0 and r=−1r = -1 (-1 should not be confused with a reverse-index position; it can be any negative number and is used only for convenience at the start of the loop).

Procedure

Suppose we are about to compute d1[i]d_1[i] for the next ii, and all values in the preceding d1[]d_1[] have already been computed. We compute it as follows:

  • If ii lies outside the current palindromic substring, namely i>ri > r, we invoke the naive algorithm.

    Thus, we increase d1[i]d_1[i] continuously, checking at each step whether the current substring [i−d1[i]…i+d1[i]][i - d_1[i] \dots i + d_1[i]] (d1[i]d_1[i] denotes the radius here and below) is a palindrome. The algorithm stops when it finds the first pair of corresponding characters that differ, or when it reaches a boundary of ss. In either case, we have computed d1[i]d_1[i]. We must then remember to update (l,r)(l, r).

  • Now consider the case i≤ri \le r. We will try to obtain information from the values of d1[]d_1[] that have already been computed. First, reflect position ii within the palindromic substring (l,r)(l, r); this gives us j=l+(r−i)j = l + (r - i). Now consider the value d1[j]d_1[j]. Since position jj is symmetric to position ii, we can almost always set d1[i]=d1[j]d_1[i] = d_1[j]. The idea is illustrated below (you can think of the palindrome centered at jj as being “copied” to the position centered at ii):

    … sl … sj−d1[j]+1 … sj … sj+d1[j]−1⏟palindrome … si−d1[j]+1 … si … si+d1[j]−1⏟palindrome … sr⏞palindrome …\ldots\ \overbrace{ s_l\ \ldots\ \underbrace{ s_{j-d_1[j]+1}\ \ldots\ s_j\ \ldots\ s_{j+d_1[j]-1} }_\text{palindrome}\ \ldots\ \underbrace{ s_{i-d_1[j]+1}\ \ldots\ s_i\ \ldots\ s_{i+d_1[j]-1} }_\text{palindrome}\ \ldots\ s_r }^\text{palindrome}\ \ldots

    However, there is a tricky case that must be handled correctly: when the “inner” palindrome reaches the boundary of the “outer” palindrome, that is, j−d1[j]+1≤lj - d_1[j] + 1 \le l (or equivalently, i+d1[j]−1≥ri + d_1[j] - 1 \ge r). Since symmetry outside the range of the “outer” palindrome is not guaranteed, directly setting d1[i]=d1[j]d_1[i] = d_1[j] would be incorrect: we do not have enough information to assert that the palindrome at position ii has the same length.

    In fact, to handle this case correctly, we should “truncate” the length of the palindrome by setting d1[i]=r−i+1d_1[i] = r - i + 1. We then run the naive algorithm to try to increase the value of d1[i]d_1[i] as much as possible.

    The case is illustrated below (the palindrome centered at jj has been truncated so that it lies within the “outer” palindrome):

    … sl … sj … sj+(j−l)⏟palindrome … si−(r−i) … si … sr⏟palindrome⏞palindrome ……………⏟try moving here\ldots\ \overbrace{ \underbrace{ s_l\ \ldots\ s_j\ \ldots\ s_{j+(j-l)} }_\text{palindrome}\ \ldots\ \underbrace{ s_{i-(r-i)}\ \ldots\ s_i\ \ldots\ s_r }_\text{palindrome} }^\text{palindrome}\ \underbrace{ \ldots \ldots \ldots \ldots \ldots }_\text{try moving here}

    The illustration shows that although the palindrome centered at jj may be longer and extend beyond the “outer” palindrome, at position ii we can use only the part that lies entirely within the “outer” palindrome. However, the answer at position ii may be larger than this value, so we next run the naive algorithm to try to extend it beyond the “outer” palindrome, into the region marked “try moving here.”

Finally, it is worth emphasizing once more that we must remember to update (l,r)(l, r) after computing each d1[i]d_1[i].

Let us also repeat that the algorithm for computing the even-length palindrome array d2[]d_2[] is very similar to the algorithm above for computing the odd-length palindrome array d1[]d_1[].

Complexity of Manacher's algorithm

Since we always run the naive algorithm when computing the answer for a particular position, the fact that the algorithm has linear time complexity is not obvious at first glance.

However, a closer analysis shows that the algorithm is linear. Here we should point out that the algorithm for computing the Z-function is quite similar to this algorithm and also has linear time complexity.

In fact, observe that every iteration of the naive algorithm increases rr by 11, and that rr never decreases during the execution of the algorithm. These two observations tell us that the naive algorithm performs O(n)O(n) iterations in total.

The other part of Manacher's algorithm is clearly linear as well, so the total complexity is O(n)O(n).

Implementation of Manacher's algorithm

Case analysis

To compute d1[]d_1[], we use the following code:

C++

vector<int> d1(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 (0 <= i - k && i + k < n && s[i - k] == s[i + k]) {
    k++;
  }
  d1[i] = k--;
  if (i + k > r) {
    l = i - k;
    r = i + k;
  }
}

Python

d1 = [0] * n
l, r = 0, -1
for i in range(0, n):
    k = 1 if i > r else min(d1[l + r - i], r - i + 1)
    while 0 <= i - k and i + k < n and s[i - k] == s[i + k]:
        k += 1
    d1[i] = k
    k -= 1
    if i + k > r:
        l = i - k
        r = i + k

The code for computing d2[]d_2[] is very similar, but some arithmetic expressions are slightly different:

C++

vector<int> d2(n);
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 (0 <= i - k - 1 && 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;
  }
}

Python

d2 = [0] * n
l, r = 0, -1
for i in range(0, n):
    k = 0 if i > r else min(d2[l + r - i + 1], r - i + 1)
    while 0 <= i - k - 1 and i + k < n and s[i - k - 1] == s[i + k]:
        k += 1
    d2[i] = k
    k -= 1
    if i + k > r:
        l = i - k - 1
        r = i + k

Unified treatment

Although we considered the computation of d1[]d_1[] and d2[]d_2[] separately in the explanation and implementations above, a trick can actually unify their computation as the computation of d1[]d_1[].

Given a string ss of length nn, we insert separators #\# into its n+1n + 1 gaps, thereby constructing a string s′s' of length 2n+12n + 1. For example, for s=abababcs = \mathtt{abababc}, the corresponding string is s′=#a#b#a#b#a#b#c#s' = \mathtt{\#a\#b\#a\#b\#a\#b\#c\#}.

A #\# between two letters represents the corresponding “gap” in ss. The #\# characters at the two ends are included for implementation convenience.

Notice that after computing d1[]d_1[] for s′s', for any position ii, the longest palindromic substring described by d1[i]d_1[i] must end in a #\# (if it ended in a letter, then there would necessarily be a #\# on each side of that letter, so it could be extended outward by one to obtain a longer palindrome). Therefore, consider a maximal palindromic substring in ss centered at a letter, and let its length be m+1m + 1. Its corresponding maximal palindromic substring in s′s' is centered at the corresponding letter and has length 2m+32m + 3; meanwhile, consider a maximal palindromic substring in ss centered at a gap, and let its length be mm. Its corresponding maximal palindromic substring in s′s' is centered at the #\# representing that gap and has length 2m+12m + 1 (in both cases above, mm is even, but whether this property holds does not affect the conclusion). Combining these observations with a small amount of calculation, it is easy to see that in s′s', d1[i]d_1[i] represents the total length plus one of the maximal palindromic substring centered at the corresponding position in ss.

The conclusion above establishes the relationship between d1[]d_1[] for s′s' and d1[]d_1[] and d2[]d_2[] for ss.

Since this unified treatment is essentially just computing d1[]d_1[] for s′s', after obtaining s′s', the code is the same as the code for computing d1[]d_1[] in the previous section.

Practice problems

This page is primarily translated from the blog post Нахождение всех подпалиндромов and its English translation, Finding all sub-palindromes in O(N)O(N). The Russian version is licensed under Public Domain + Leave a Link; the English version is licensed under CC-BY-SA 4.0.

This content comes from OI Wiki: Manacher. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.