Manacher's Algorithm β Longest Palindromic Substring in O(n)
- Separator insertion converts all palindromes to odd-length β canonical technique, not a hack. One unified case beats branching on odd/even in clarity and implementation.
- P[i] = palindrome radius. Rightmost boundary [c, r] only advances rightward β total expansion work O(n). If r never advances, you are not implementing Manacher.
- Initialise P[i] = min(r-i, P[mirror]) when i < r. Then try to expand. Update c and r if extended beyond old r. These four lines are the core.
LeetCode 5 β Longest Palindromic Substring β is one of the most common interview problems. The O(n^2) expand-around-centre solution passes most judges. Manacher's O(n) solution demonstrates you understand linear string algorithms at a structural level, not just memorised patterns.
Manacher (1975) is not widely taught in undergraduate curricula, which means knowing it distinguishes you. The underlying technique β maintaining a rightmost boundary and exploiting mirror symmetry β is the exact same structural insight as KMP and Z-algorithm. Once you see this pattern, you recognise it everywhere.
The Even/Odd Problem and the Transformation
Palindromes come in two flavours: odd-length ('aba') and even-length ('abba'). Handling both cases separately doubles code complexity. Manacher's standard trick: insert a separator character between every character (and at boundaries) to convert all palindromes to odd-length.
Original: 'abba' Transformed: '#a#b#b#a#'
Now every palindrome in the original maps to an odd-length palindrome in the transformed string. '#b#b#' is the palindrome for 'bb'. '#a#b#b#a#' is the palindrome for 'abba'. We only need to handle one case.
def transform(s: str) -> str: return '#' + '#'.join(s) + '#' print(transform('abba')) # '#a#b#b#a#' print(transform('racecar'))# '#r#a#c#e#c#a#r#'
#r#a#c#e#c#a#r#
The P Array β Palindrome Radii
For the transformed string T, we compute P[i] = the radius of the longest palindrome centred at i (not counting the centre itself). So P[i] = k means T[i-k..i+k] is a palindrome.
The length of the corresponding original palindrome is P[i] (the separator characters cancel out).
For T = '#a#b#b#a#': - P[0]=0 ('#') - P[1]=1 ('#a#') - P[2]=0 ('a') - P[3]=3 ('#a#b#b#a#') β full string - P[4]=3 (same, centre is '#' between two b's) - etc.
Manacher's Algorithm β Full Implementation
Manacher maintains a 'mirror window' [l, r] β the palindrome with the rightmost boundary seen so far (centre c, right edge r). For each i, if i < r, initialise P[i] from the mirror position (2*c - i). Then try to expand.
def manacher(s: str) -> str: # Transform to handle even/odd uniformly T = '#' + '#'.join(s) + '#' n = len(T) P = [0] * n c = r = 0 # centre and right boundary of rightmost palindrome for i in range(n): mirror = 2 * c - i # mirror of i around c if i < r: P[i] = min(r - i, P[mirror]) # Try to expand around i left, right = i - P[i] - 1, i + P[i] + 1 while left >= 0 and right < n and T[left] == T[right]: P[i] += 1 left -= 1 right += 1 # Update rightmost palindrome boundary if i + P[i] > r: c, r = i, i + P[i] # Find maximum in P max_len, centre = max((v, i) for i, v in enumerate(P)) start = (centre - max_len) // 2 return s[start : start + max_len] print(manacher('babad')) # 'bab' or 'aba' print(manacher('cbbd')) # 'bb' print(manacher('racecar')) # 'racecar' print(manacher('abacabadabacaba')) # 'abacabadabacaba'
bb
racecar
abacabadabacaba
Why it's O(n) β The Rightmost Boundary Argument
The r pointer only ever moves right β never decreases. Each time we expand (the while loop), r increases. So the total number of expansions across all iterations is at most n. All other operations per iteration are O(1). Total: O(n).
This is the same argument as KMP (pointer only moves forward) and Z-algorithm (r only moves right). Recognising this pattern is key to understanding linear string algorithms.
Finding All Palindromic Substrings
Manacher computes all palindrome radii in one pass, so you can use P to answer queries like 'is s[l..r] a palindrome?' in O(1) after O(n) preprocessing.
def all_palindrome_lengths(s: str) -> list[int]: """Return length of longest palindrome centred at each position.""" T = '#' + '#'.join(s) + '#' n = len(T) P = [0] * n c = r = 0 for i in range(n): if i < r: P[i] = min(r - i, P[2*c - i]) while i-P[i]-1 >= 0 and i+P[i]+1 < n and T[i-P[i]-1] == T[i+P[i]+1]: P[i] += 1 if i + P[i] > r: c, r = i, i + P[i] # Extract only original positions (every other entry in P) return [P[2*i+1] for i in range(len(s))] # odd centres print(all_palindrome_lengths('abacaba')) # [1, 3, 1, 7, 1, 3, 1] β longest palindrome at each centre
π― Key Takeaways
- Separator insertion converts all palindromes to odd-length β canonical technique, not a hack. One unified case beats branching on odd/even in clarity and implementation.
- P[i] = palindrome radius. Rightmost boundary [c, r] only advances rightward β total expansion work O(n). If r never advances, you are not implementing Manacher.
- Initialise P[i] = min(r-i, P[mirror]) when i < r. Then try to expand. Update c and r if extended beyond old r. These four lines are the core.
- The rightward-only pointer argument proves O(n) for KMP, Z, and Manacher. This unifying structure is the real lesson β all three are the same insight in different forms.
- In interviews: O(n^2) expand-around-centre passes most judges. Manacher is the follow-up question that filters strong candidates.
Interview Questions on This Topic
- QWhy do we insert separator characters in Manacher's algorithm?
- QWhat does P[i] represent, and how is it used to recover the original palindrome?
- QExplain why Manacher's algorithm is O(n) and not O(nΒ²).
- QHow is Manacher's algorithm similar in structure to KMP and the Z-algorithm?
Frequently Asked Questions
Is Manacher's algorithm commonly asked in interviews?
Yes β LeetCode problem 5 'Longest Palindromic Substring' is a top interview problem. The O(nΒ²) expand-around-centre solution is acceptable for most interviews, but Manacher's O(n) solution demonstrates strong algorithmic understanding.
What separator character should I use?
Any character guaranteed not to appear in the input. '#' is conventional. For Unicode strings, you can use a character outside the input alphabet. The separator ensures two different separator positions can never form a match.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.