Manacher's Algorithm
Compute the palindrome radius around every center in O(n) by reusing mirrored radii inside the rightmost known palindrome.
Overview
Manacher's algorithm finds the longest palindromic substring — and in fact the maximal palindrome centered at every position — in linear time. The plain center-expansion method tries each of the 2n - 1 centers and expands outward, which is O(n²) on inputs like "aaaa…a". Manacher keeps the expansion but skips the part that a previously found palindrome already proves.
To treat odd- and even-length palindromes uniformly, the string is transformed by inserting a separator between characters: "abba" becomes "^#a#b#b#a#$". In the transformed string every palindrome has odd length and a well-defined center, and the radius p[i] in the transformed string equals the length of the original palindrome.
Intuition
A mental model before the formal terms.
A palindrome is symmetric about its center. If you are standing inside a big palindrome [l, r] centered at c, then whatever is at position i looks exactly like what is at the mirror position 2c - i. So a small palindrome around the mirror implies an equally large palindrome around i — at least as far as the big palindrome's boundary. That is the mirror trick: copy the mirror's radius, capped at the distance to r, and only expand beyond that boundary with real character comparisons.
Every real comparison either fails (ending this center) or extends r further right. Since r only moves right and stops at n, the total number of successful comparisons is O(n).
How it works
- Build
t = "^#" + "#".join(s) + "#$". The sentinels^and$never match anything, so expansion needs no bounds checks. - Keep
c(center) andr(right edge) of the palindrome that currently reaches furthest right. For eachifrom 1 tolen(t) - 2: letmirror = 2c - i. Ifi < r, initialisep[i] = min(r - i, p[mirror]); otherwisep[i] = 0. - Center expansion: while
t[i + p[i] + 1] == t[i - p[i] - 1], incrementp[i]. - If
i + p[i] > r, this palindrome extends further right than any before: setc = i,r = i + p[i]. - The answer is the maximum
p[i]; in the original string it starts at(i - p[i]) / 2and has lengthp[i].
Why it works
Let the palindrome centered at c span [c - p[c], c + p[c]] and let i lie inside it with mirror j = 2c - i. Reflecting through c maps the substring around j onto the substring around i reversed; so any palindrome around j that stays within [c - p[c], c + p[c]] reflects to a palindrome of the same radius around i. That is why p[i] ≥ min(p[j], r - i). Beyond r nothing is known, hence the cap and the explicit expansion.
If the mirror's palindrome is strictly shorter than r - i, then p[i] = p[j] exactly (a longer one around i would reflect to a longer one around j), and the expansion loop fails immediately. Otherwise the expansion starts at r and every success increases r. So the amortised cost is O(n).
Recognition
How to tell a problem wants this.
- The problem asks for the longest palindromic substring, or for the number of palindromic substrings, with
nup to10^5or more. - You need the maximal palindrome centered at every position (e.g. counting palindromic substrings, palindromic partition speed-ups).
- Center expansion is the obvious approach but the input can be a single repeated character.
Interactive visualization
Play, step, change the input. ← → and space work too.
1t = "#" + "#".join(s) + "#"; P = [0]*len(t); c = r = 02for i in 0 .. len(t)-1:3 mirror = 2*c - i4 if i < r: P[i] = min(r - i, P[mirror])5 while t[i + P[i] + 1] == t[i - P[i] - 1]: P[i] += 16 if i + P[i] > r: c = i; r = i + P[i]7answer = longest P[i]; palindrome = s[(i - P[i]) / 2 .. +P[i]]Pseudocode
1t = "^#" + "#".join(s) + "#$"; p = zeros; c = r = 02for i in 1 .. len(t) - 2:3 mirror = 2c - i4 if i < r: p[i] = min(r - i, p[mirror])5 while t[i + p[i] + 1] == t[i - p[i] - 1]: p[i] += 16 if i + p[i] > r: c = i, r = i + p[i]7best = argmax p; start = (best - p[best]) / 2; return s[start .. start + p[best])Implementations
1def longest_palindrome(s: str) -> str:2 if not s:3 return ""41 · Transform with separators and sentinels5 t = "^#" + "#".join(s) + "#$"6 n = len(t)7 p = [0] * n # palindrome radius around each center of t8 c = r = 0 # center and right edge of the rightmost palindrome9 for i in range(1, n - 1):102 · Mirror the radius inside the current palindrome11 mirror = 2 * c - i12 if i < r:13 p[i] = min(r - i, p[mirror])143 · Expand around the center15 while t[i + p[i] + 1] == t[i - p[i] - 1]:16 p[i] += 1174 · Update the rightmost palindrome18 if i + p[i] > r:19 c, r = i, i + p[i]205 · Map the best radius back to the original string21 best = max(range(1, n - 1), key=lambda i: p[i])22 start = (best - p[best]) // 223 return s[start:start + p[best]]"#".join(s)treats the string as an iterable of characters and inserts the separator between all of them — one linear pass, no explicit loop.p = [0] * nandc = r = 0set up the radius array and the rightmost-palindrome bookkeeping.- The
whileloop is the only place characters are compared; the sentinels^and$guarantee it terminates without index errors. max(range(1, n - 1), key=lambda i: p[i])returns the index of the best center directly.s[start:start + p[best]]slices the answer out of the original string; the radius intequals the length ins.
The final slice copies the answer (Python slices always copy); return (start, length) to avoid it.
str.joinis the canonical linear-time way to interleave;+=in a loop is quadratic in principle (CPython sometimes optimises it, but do not rely on that).//keepsstartanint; a single/would produce afloatand break slicing.max(iterable, key=...)avoids the manual argmax loop other languages need.
- Using
/instead of//for the start index and gettingTypeError: slice indices must be integers. - Running
ioverrange(n)including the sentinels, which expands out of bounds. - Taking
max(p)(the radius) when the problem needs the substring — the index is what maps back.
- Building the transformed string: Python
"#".join(s)and JS/TSsplit/joinare single-pass; C+++=into a reservedstd::stringavoids the intermediate array the script languages create. - Integer division for the back-mapping: Python needs
//, C++intdivision truncates natively, JS/TS/happens to be exact here because the operands share parity — butMath.floordocuments intent. - Unicode: the transform indexes code units in JS/TS and code points in Python; C++ works on bytes. For non-ASCII input the three can disagree about what a "character" is — normalise or split by code points first.
substr(start, length)in C++ vssubstring(start, end)in JS/TS vs slices[a:b]in Python — three different conventions for extracting the final answer.
Complexity
The transformed string and radius array are each about 2n + 3 entries.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Longest palindromic substring with large
n. - Counting all palindromic substrings: the answer is
Σ ⌈p[i] / 2⌉over the transformed string. - Any problem needing the maximal palindrome radius at every center as a preprocessing step.
- Small inputs (
n ≤ 1000) whereO(n²)center expansion is simpler and fast enough. - Palindromic subsequences — that is a Subsequence DP problem, not a substring one.
- Palindromes under edits or with wildcards — Manacher assumes exact symmetry.
Alternatives
Common mistakes
- Omitting the sentinels
^and$and then reading outside the array during expansion. - Using
i <= rinstead ofi < r(or mixing up inclusive and exclusiver), which copies a radius the current palindrome does not vouch for. - Forgetting to cap
p[mirror]byr - i. - Converting back to the original string incorrectly — the start index is
(i - p[i]) / 2and the length isp[i], not2p[i] + 1.
Interview patterns
- Longest palindromic substring in
O(n)(follow-up after theO(n²)expansion solution). - Count palindromic substrings.
- Shortest palindrome by prepending characters: longest palindromic prefix.
- Palindrome pairs and palindromic partitioning with precomputed radii.
- Recognizing the approach from an array and a targetIntermediate
- When space complexity mattersIntermediate
- Two pointers or hash map?Intermediate
- Convincing me your algorithm is correctExpert
- Two SumBeginner