String Algorithms
Find all palindromic substrings in O(n) by maintaining the rightmost palindrome boundary and reusing mirror information. Upgrades expand-around-center from O(n²) to linear time.
The classic palindrome problem, LeetCode 5. Longest Palindromic Substring, asks you to find the longest palindromic substring in a given string. The expand-around-center approach solves it in time by trying every possible center and expanding outward. That is clean and interview-friendly, but there is a linear-time algorithm that solves the same problem in : Manacher's Algorithm. It works by reusing palindrome information already computed, avoiding redundant expansions entirely.
Odd-length palindromes like "aba" center on a single character, while even-length palindromes like "abba" center between two characters. Handling both cases complicates the logic. Manacher's algorithm eliminates this distinction with a simple preprocessing trick. Insert a special character (typically '#') between every character and at both ends.
For example, "abba" becomes "#a#b#b#a#". Now every palindrome, whether originally odd or even, becomes an odd-length palindrome centered on a single position in the transformed string. The even palindrome "abba" becomes "#a#b#b#a#" centered on the middle '#' between the two 'b's, with an odd length of 9.
def preprocess(s: str) -> str:
return '#' + '#'.join(s) + '#'"abc" becomes "#a#b#c#" (length ). A palindrome radius of in the transformed string corresponds to a palindrome of length in the original string.
Manacher's algorithm maintains two key variables as it scans left to right:
For each new position i, we build an array p[] where p[i] is the radius of the longest palindrome centered at i in the transformed string.
The key insight: if i < R, then i lies inside the palindrome centered at C. Its mirror position is mirror = 2 * C - i. Because palindromes are symmetric, p[mirror] gives us a lower bound for p[i]. We can initialize p[i] = min(p[mirror], R - i) and only expand beyond that. The min with R - i is needed because the mirror's palindrome might extend beyond the left edge of the palindrome centered at C, and we have no information past R.
If i >= R, we have no information and start with p[i] = 0, expanding from scratch.
After computing p[i], if i + p[i] > R, we update C = i and R = i + p[i].
Let us trace through s = "cbbd". The transformed string is t = "#c#b#b#d#" (indices 0 through 8).
Initialize C = 0, R = 0, p = [0, 0, 0, 0, 0, 0, 0, 0, 0].
#): i >= R, so p[0] = 0. No expansion possible (left boundary). C=0, R=0.c): i >= R, p[1] = 0. Try expand: t[0]='#' vs t[2]='#', which match, so p[1] = 1. t[-1] is out of bounds, stop. Update C=1, R=2.#): i >= R, p[2] = 0. t[1]='c' vs t[3]='b', no match. C=1, R=2.b): i >= R, p[3] = 0. t[2]='#' vs t[4]='#' match, so p[3] = 1. t[1]='c' vs t[5]='b', no match. Update C=3, R=4.#): i < R (4 < 4 is false), so p[4] = 0. t[3]='b' vs t[5]='b' match, so p[4] = 1. t[2]='#' vs t[6]='#' match, so p[4] = 2. t[1]='c' vs t[7]='d', no match. Update C=4, R=6.b): i < R (5 < 6). Mirror = 2*4 - 5 = 3, p[3] = 1. p[5] = min(1, 6-5) = 1. Try expand: t[3]='b' vs t[7]='d', no match. C=4, R=6.#): i >= R, p[6] = 0. t[5]='b' vs t[7]='d', no match. Stays 0.d): i >= R, p[7] = 0. t[6]='#' vs t[8]='#' match, so p[7] = 1. Out of bounds on right, stop. Update C=7, R=8.#): i >= R, p[8] = 0. Out of bounds, stop.Final p = [0, 1, 0, 1, 2, 1, 0, 1, 0]. The maximum is p[4] = 2 at index 4. The palindrome in the original string has length 2 and is centered at original index (4 - 1) / 2 = 1. Wait, let us compute the substring properly. The center in the transformed string is index 4, which corresponds to the '#' between the two 'b's. The original start index is (4 - 2) // 2 = 1 and the palindrome is s[1:3] = "bb". Correct.
def manacher(s: str) -> str:
t = '#' + '#'.join(s) + '#'
n = len(t)
p = [0] * n
C = R = 0
for i in range(n):
mirror = 2 * C - i
if i < R:
p[i] = min(p[mirror], R - i)
# Attempt expansion
while (i + p[i] + 1 < n and i - p[i] - 1 >= 0
and t[i + p[i] + 1] == t[i - p[i] - 1]):
p[i] += 1
# Update center and right boundary
if i + p[i] > R:
C = i
R = i + p[i]
# Find the maximum radius and recover the substring
max_radius = max(p)
center = p.index(max_radius)
start = (center - max_radius) // 2
return s[start : start + max_radius]The conversion back to the original string uses the fact that a radius of r in the transformed string corresponds to a palindrome of length r in the original string, starting at index (center - r) // 2.
The critical observation is that the right boundary R never moves left. It only increases. Each time we expand a palindrome past R, R advances by the number of new characters compared. A character can only contribute to expansion once (when R first passes over it). Since R can advance at most times total, the total number of character comparisons across all expansions is .
When i < R and we initialize p[i] from the mirror, no expansion is needed if the mirror palindrome fits entirely within [C - p[C], C + p[C]]. This is the "free" information that avoids the trap of expand-around-center. We only pay for expansion when pushing R further right, and each position is pushed past at most once.
Consider LeetCode 647. Palindromic Substrings, which requires the total count of palindromic substrings. The expand-around-center approach solves this in , but with the Manacher radius array we can do it in .
Each entry p[i] in the transformed string represents a palindrome of radius p[i]. How many palindromic substrings does this account for? A position centered on an original character (odd index in the transformed string) with radius r contributes (r + 1) // 2 odd-length palindromes. A position centered on a '#' (even index) with radius r contributes r // 2 even-length palindromes. More simply, each center in the transformed string with radius p[i] contributes ceil(p[i] / 2) palindromic substrings to the total count, which equals (p[i] + 1) // 2.
def count_substrings(s: str) -> int:
t = '#' + '#'.join(s) + '#'
n = len(t)
p = [0] * n
C = R = 0
for i in range(n):
mirror = 2 * C - i
if i < R:
p[i] = min(p[mirror], R - i)
while (i + p[i] + 1 < n and i - p[i] - 1 >= 0
and t[i + p[i] + 1] == t[i - p[i] - 1]):
p[i] += 1
if i + p[i] > R:
C = i
R = i + p[i]
return sum((r + 1) // 2 for r in p)Take LeetCode 214. Shortest Palindrome: given a string s, find the shortest palindrome you can form by adding characters only to the front. The key insight is that you need the longest palindromic prefix of s. Whatever portion of s is already a palindrome starting from index 0, you keep; the remaining suffix gets reversed and prepended.
After running Manacher's on the transformed string, scan the radius array to find the rightmost center whose palindrome starts at position 0 in the original string. For a center at index c with radius p[c], the palindrome in the original string spans from (c - p[c]) // 2 to (c + p[c]) // 2 - 1. We need the one where the start is 0 and the length is maximized.
def shortest_palindrome(s: str) -> str:
if not s:
return s
t = '#' + '#'.join(s) + '#'
n = len(t)
p = [0] * n
C = R = 0
for i in range(n):
mirror = 2 * C - i
if i < R:
p[i] = min(p[mirror], R - i)
while (i + p[i] + 1 < n and i - p[i] - 1 >= 0
and t[i + p[i] + 1] == t[i - p[i] - 1]):
p[i] += 1
if i + p[i] > R:
C = i
R = i + p[i]
# Find longest palindromic prefix
longest = 0
for i in range(n):
# Check if this palindrome starts at position 0 in original string
if i - p[i] == 0:
longest = max(longest, p[i])
suffix = s[longest:]
return suffix[::-1] + sThe condition i - p[i] == 0 checks that the palindrome in the transformed string starts at the very beginning (index 0 of t is '#', and (0) // 2 = 0 in the original string). The palindrome length in the original string is p[i], so we reverse everything after s[:longest] and prepend it.
R.p.You're probably looking at Manacher's when:
Common templates:
(p[i] + 1) // 2 across the radius array. Example: Palindromic Substrings.