Description
Given a string of length , find all pairs such that the substring is a palindrome. A string is a palindrome when ( is the reverse of ).
Explanation
There may be 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 , we determine the values and . They represent, respectively, the number of odd-length and even-length palindromic substrings centered at position . Equivalently, they represent the radius of the longest palindrome centered at position (the radius lengths and are both the number of characters from position through the rightmost position of the palindrome).
For example, the string has three odd-length palindromic substrings centered at . The longest palindrome has radius , so :
The string has two even-length palindromic substrings centered at . The longest palindrome has radius , so :
The key idea is that if we have a palindrome of length centered at some position , then we also have palindromes of lengths , , and so on centered at . Thus, the two arrays and 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, and . 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 time, while using a suffix array and fast LCA, it can be solved in 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 , after comparing a pair of corresponding characters, it tries to increase the answer by whenever possible.
This algorithm is rather slow: it can compute the answer only in 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 ; the algorithm for finding all even-length palindromic substrings (that is, computing ) requires only a few small modifications to the odd-length case.
To compute the values quickly, we maintain the boundaries of the rightmost palindromic substring found (that is, the palindrome with the maximum value of , where and are the positions of its left and right boundaries, respectively). Initially, we set and (-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 for the next , and all values in the preceding have already been computed. We compute it as follows:
-
If lies outside the current palindromic substring, namely , we invoke the naive algorithm.
Thus, we increase continuously, checking at each step whether the current substring ( 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 . In either case, we have computed . We must then remember to update .
-
Now consider the case . We will try to obtain information from the values of that have already been computed. First, reflect position within the palindromic substring ; this gives us . Now consider the value . Since position is symmetric to position , we can almost always set . The idea is illustrated below (you can think of the palindrome centered at as being “copied” to the position centered at ):
However, there is a tricky case that must be handled correctly: when the “inner” palindrome reaches the boundary of the “outer” palindrome, that is, (or equivalently, ). Since symmetry outside the range of the “outer” palindrome is not guaranteed, directly setting would be incorrect: we do not have enough information to assert that the palindrome at position has the same length.
In fact, to handle this case correctly, we should “truncate” the length of the palindrome by setting . We then run the naive algorithm to try to increase the value of as much as possible.
The case is illustrated below (the palindrome centered at has been truncated so that it lies within the “outer” palindrome):
The illustration shows that although the palindrome centered at may be longer and extend beyond the “outer” palindrome, at position we can use only the part that lies entirely within the “outer” palindrome. However, the answer at position 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 after computing each .
Let us also repeat that the algorithm for computing the even-length palindrome array is very similar to the algorithm above for computing the odd-length palindrome array .
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 by , and that never decreases during the execution of the algorithm. These two observations tell us that the naive algorithm performs iterations in total.
The other part of Manacher's algorithm is clearly linear as well, so the total complexity is .
Implementation of Manacher's algorithm
Case analysis
To compute , 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 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 and separately in the explanation and implementations above, a trick can actually unify their computation as the computation of .
Given a string of length , we insert separators into its gaps, thereby constructing a string of length . For example, for , the corresponding string is .
A between two letters represents the corresponding “gap” in . The characters at the two ends are included for implementation convenience.
Notice that after computing for , for any position , the longest palindromic substring described by 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 centered at a letter, and let its length be . Its corresponding maximal palindromic substring in is centered at the corresponding letter and has length ; meanwhile, consider a maximal palindromic substring in centered at a gap, and let its length be . Its corresponding maximal palindromic substring in is centered at the representing that gap and has length (in both cases above, 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 , represents the total length plus one of the maximal palindromic substring centered at the corresponding position in .
The conclusion above establishes the relationship between for and and for .
Since this unified treatment is essentially just computing for , after obtaining , the code is the same as the code for computing in the previous section.
Practice problems
- UVa #11475 "Extend to Palindrome"
- The Longest Double Palindromic Substring (National Training Team)
- CF1326D2. Labyrinth
This page is primarily translated from the blog post Нахождение всех подпалиндромов and its English translation, Finding all sub-palindromes in . The Russian version is licensed under Public Domain + Leave a Link; the English version is licensed under CC-BY-SA 4.0.