Algorithm tutorials
Manacher's Algorithm (All Palindromes in O(n))
Manacher's algorithm computes the longest palindrome centered at every position in linear time by mirroring inside the rightmost palindrome found so far: longest palindromic substring, palindrome counting and O(1) palindrome queries.
This is an extension topic beyond most interviews: expand-around-center (O(n^2)) is the expected answer for palindromic substrings, and Manacher pays off as the follow-up when n reaches 10^5 or more, or when a hard problem needs the palindrome radius at every center.
Intuition
Every palindrome has a center: a character (odd length) or a gap between two characters (even length). If the longest palindrome around a center has length L, then trimming one character from each end gives palindromes of length L - 2, L - 4, and so on. So one number per center, its radius, describes every palindrome in the string, even though there can be O(n^2) of them.
To treat odd and even centers alike, insert a separator: "aba" becomes t = "#a#b#a#". Now every palindrome of s is an odd-length palindrome of t centered at a character or a #.
The speed-up is mirroring. Keep the palindrome of t that reaches farthest to the right, t[l:r]. For a new center i < r, its mirror j = l + r - 1 - i was computed already, and inside the big palindrome the neighborhood of i is a reflection of the neighborhood of j. So the radius at i is at least min(p[j], r - i); only characters beyond r need direct comparison, and each such comparison moves r right. r never moves back, so the whole scan is O(n).
When to use it in interviews / OAs
- Longest palindromic substring when
nis large, or as the "can you do better thanO(n^2)?" follow-up. - Counting palindromic substrings in
O(n). - Many "is
s[l:r]a palindrome?" queries:O(1)each after oneO(n)pass (hashing also works but is probabilistic). - Hard problems built on per-center radii: maximum product of two non-overlapping odd palindromes, shortest palindrome by adding characters (a palindromic prefix is a palindrome that touches index 0).
Template
Radii on the separated string
def manacher(s: str) -> list[int]:
# t = "#a#b#...#"; p[i] = arm length so that t[i - p[i] + 1 : i + p[i]] is a palindrome
t = "#" + "#".join(s) + "#"
n = len(t)
p = [0] * n
l = r = 0 # t[l:r] is the palindrome reaching farthest right
for i in range(n):
k = min(p[l + r - 1 - i], r - i) if i < r else 1 # start from the mirror's radius
while i - k >= 0 and i + k < n and t[i - k] == t[i + k]:
k += 1
p[i] = k
if i + k > r:
l, r = i - k + 1, i + k
return p
The palindrome of t around index i corresponds to a palindrome of s of length p[i] - 1 starting at index (i - p[i] + 1) // 2.
What you can read off the radii
def longest_palindrome_manacher(s: str) -> str:
p = manacher(s)
i = max(range(len(p)), key=p.__getitem__)
start = (i - p[i] + 1) // 2
return s[start:start + p[i] - 1]
def count_palindromic_substrings(s: str) -> int:
# center i in t contributes palindromes of lengths p[i] - 1, p[i] - 3, ... > 0
return sum(k // 2 for k in manacher(s))
def palindrome_checker(s: str):
p = manacher(s)
def is_pal(l: int, r: int) -> bool: # is s[l:r] a palindrome? (l < r)
return p[l + r] - 1 >= r - l # its center in t sits at index l + r
return is_pal
Complexity
O(n) time and O(n) memory: the separated string has 2n + 1 characters and every successful comparison advances r. Each query on top is O(1).
For comparison, expand-around-center is O(n^2) time and O(1) memory; it is shorter to write and is what most interviewers expect first.
Common pitfalls
- Forgetting the even case. Without separators (or a second array for even centers), palindromes like
"abba"are missed. - Index conversion. Lengths and start positions in
scome from the formulas above; off-by-one errors here are the usual bug, so test"a","aa","aba"and the empty string. - Separator characters. The
#must not occur ins. Since it is only compared with itself at even positions, any character absent fromsworks. - Mirror outside the window. Use
min(p[mirror], r - i); trustingp[mirror]blindly copies information about characters that lie beyond the known palindrome. - Counting palindromes as distinct strings.
count_palindromic_substringscounts occurrences (positions). Distinct palindromes need a different structure (a palindromic tree), beyond interview scope.
Interview variations
- Longest palindromic substring / palindromic substrings count. The two helpers above.
- Palindrome partitioning (minimum cuts). Precompute
is_palinO(n)instead of anO(n^2)table, then run the usual DP. - Shortest palindrome by adding characters in front. Find the longest palindromic prefix: the largest
p[i]whose palindrome starts at index 0 oft; KMP ons + "#" + reverse(s)is the classic alternative. - Maximum product of two non-overlapping odd palindromes. Odd radii from Manacher, then prefix and suffix maxima of "longest palindrome ending by / starting from each index".
- Hashing alternative. Forward and reverse rolling hashes answer palindrome queries in
O(1)with a small error probability, and combine with binary search for the longest palindrome around each center inO(n log n).