Coding Interview Patterns Module 28 – String patterns
Longest palindromic substring
Find the longest palindromic substring by expanding around its 2n − 1 centres, count every palindrome, and see when the linear Manacher method pays.
What you will learn
- Find the longest palindromic substring by expanding around all 2n − 1 centres
- Prove that expanding each centre until its ends differ finds every palindrome
- Count every palindromic substring and keep the total in a 64-bit integer
- Explain how Manacher's algorithm reuses mirror positions to run in linear time
- Choose between centre expansion and Manacher's algorithm from the size and shape of the input
Before you start
What you’ll learn · Before you start
What you will learn
- Find the longest palindromic substring by expanding around all 2n − 1 centres
- Prove that expanding each centre until its ends differ finds every palindrome
- Count every palindromic substring and keep the total in a 64-bit integer
- Explain how Manacher's algorithm reuses mirror positions to run in linear time
- Choose between centre expansion and Manacher's algorithm from the size and shape of the input
Before you start
On this page
The longest palindromic substring is the longest unbroken stretch of a string that reads the same backwards, such as
TAGGAT inside TTAGGATC. Every palindrome is centred on a letter, when its length is odd, or on the gap between two
letters, when it is even, so a string of n letters has 2n − 1 centres. Widening a window from each centre while its
two ends match finds the answer in O(n²) time and O(1) extra memory, against O(n³) for checking every substring. That
is fast enough for about 10⁴ letters, and the same expansions count every palindromic substring. Manacher’s
algorithm reuses what earlier centres proved and runs in O(n) time.
The problem
The problem
A lab simulates DNA-like strands written with the letters A, C, G and T. A stretch of a strand that reads the same
backwards, letter by letter, is a mirrored segment. Biologists use the word palindrome for a different idea, a
stretch equal to its reverse complement; here it means only reading backwards. Write longest_palindrome_span(s),
which returns (start, length) of the longest mirrored segment, the one that starts first when several are equally
long, and (0, 0) for an empty strand. Limits: 0 ≤ n ≤ 10⁴.
Four examples:
"ACGTGCAT"gives(0, 7):ACGTGCA, of odd length, centred on the T at position 3."TTAGGATC"gives(1, 6):TAGGAT, of even length, centred between the two Gs."AGACTCT"gives(0, 3):AGA,CTCandTCTall have length 3, andAGAstarts first."ACGT"gives(0, 1): no letter repeats, so every palindrome is a single letter, and the first one wins.
The brute force: check every substring
Check every substring from both ends and keep the longest palindrome. A strand of n letters has n(n + 1)/2 substrings and each check makes up to n/2 comparisons, so the method is O(n³). One letter repeated is its worst case, because then every check succeeds and runs to the middle. The run counts the comparisons:
# Every substring of a strand, checked from both ends, with the character comparisons counted.
def is_palindrome(s, i, j, counter):
"""Whether s[i..j] reads the same backwards; adds each comparison it makes to counter[0]."""
while i < j:
counter[0] += 1
if s[i] != s[j]:
return False
i += 1
j -= 1
return True
def longest_by_brute_force(s):
"""(start, length) of the leftmost longest palindromic substring, and the comparisons made."""
counter = [0]
best = (0, min(len(s), 1))
for i in range(len(s)):
for j in range(i, len(s)):
if is_palindrome(s, i, j, counter) and j - i + 1 > best[1]:
best = (i, j - i + 1)
return best, counter[0]
for strand in ["ACGTGCAT", "TTAGGATC", "AGACTCT", "ACGT"]:
(start, length), work = longest_by_brute_force(strand)
print(f"{strand:9} start {start}, length {length}: {strand[start:start + length]:7} {work} comparisons")
# One repeated letter is the worst case: every check succeeds and runs to the middle.
print()
for n in (50, 100, 200):
_, work = longest_by_brute_force("A" * n)
print(f"{n} copies of A: {work:,} comparisons") Output
ACGTGCAT start 0, length 7: ACGTGCA 32 comparisons TTAGGATC start 1, length 6: TAGGAT 32 comparisons AGACTCT start 0, length 3: AGA 21 comparisons ACGT start 0, length 1: A 6 comparisons 50 copies of A: 10,725 comparisons 100 copies of A: 84,575 comparisons 200 copies of A: 671,650 comparisons
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 every_substring.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Doubling n multiplies the work by about 8, as a cubic cost does: 84,575 comparisons for 100 letters and 671,650 for 200. The same count for 10⁴ letters is 83,345,832,500, and at the rate the constraints lesson gives for C++, about 10⁸ simple steps a second, that is about 14 minutes.
The key idea: expand around every centre
Instead of guessing both ends of a palindrome, fix its centre. Start with the single letter, for an odd centre, or the two letters beside a gap, for an even one, and widen the window one letter on each side while the two new end letters match. The first mismatch ends that centre: every longer window around the same centre contains the two mismatched letters at mirrored positions, so none of them can be a palindrome. Each centre therefore finds the longest palindrome around it, and the longest of those 2n − 1 answers is the result.
One of 15 centres, expanded until its ends differ
Text description of the diagram
The diagram has two rows, one under the other.
- The strand TTAGGATC is drawn as eight cells, each holding a letter and its position from 0 to 7: T, T, A, G, G, A, T and C. A strand of 8 letters has 8 letter centres and 7 gap centres, 15 in all. The six cells from position 1 to 6, TAGGAT, have a double border: they are the longest palindrome.
- The steps of one expansion are listed in order, starting from the gap between positions 3 and 4. Positions 3 and 4 hold G and G, which match, so the palindrome has length 2. Positions 2 and 5 hold A and A, length 4. Positions 1 and 6 hold T and T, length 6. Positions 0 and 7 hold T and C, which differ, so the expansion stops. The palindrome found is TAGGAT, which starts at position 1 and has length 6.
The cost depends on how far the windows grow. On one repeated letter they grow to the ends of the strand, and 10⁴ letters cost 50,005,000 comparisons, which C++ makes in about half a second. On a random strand of A, C, G and T, a window grows past the next letter only one time in four, so the work is close to linear. The memory is two indices.
Two more uses come from the same loop. Counting every palindromic substring takes the same expansions: each step that matches adds exactly one palindrome, centred where it was found. Manacher’s algorithm finds the same palindromes in O(n) time by starting each centre from what a mirror position already proved.
A dry run on TTAGGATC
The centres run from left to right, and the best span changes only when a strictly longer palindrome appears, so the first of equally long palindromes is kept.
| Centre | Letters compared | Palindrome | Best so far |
|---|---|---|---|
| Letter 0 | none: no letter on its left | T | (0, 1) |
| Gap 0–1 | T and T match, then no letter is left | TT | (0, 2) |
| Letter 1 | T and A differ | T | (0, 2) |
| Gaps 1–2 and 2–3, letters 2 and 3 | the first two letters compared differ | A, G or none | (0, 2) |
| Gap 3–4 | G = G, A = A and T = T, then T and C differ | TAGGAT | (1, 6) |
| Letters 4 to 7, gaps 4–5 to 6–7 | the first two letters differ, or no letter is on the right | one letter or none | (1, 6) |
The answer is (1, 6). Only one of the 15 centres compared more than one pair of letters.
Exercise
Exercise · Medium · Python
Where the longest mirrored stretch starts, and how long it is
Write longest_palindrome_span(s). It returns a pair (start, length) for the longest substring of s that reads the same backwards, so that s[start:start + length] is that substring. When several are equally long, return the one that starts first. An empty string gives (0, 0).
longest_palindrome_span("ACGTGCAT") # (0, 7): ACGTGCA
longest_palindrome_span("TTAGGATC") # (1, 6): TAGGAT
longest_palindrome_span("AGACTCT") # (0, 3): AGA, the first of three of length 3
Expand around every centre: each letter, and each gap between two neighbours. One sample test has 3,000 letters, so checking every substring will run out of time. Limits: 0 ≤ n ≤ 10⁴, letters A, C, G and T.
Starter code · span.py
def longest_palindrome_span(s):
"""(start, length) of the leftmost longest palindromic substring of s; (0, 0) for ""."""
# Replace this line with your code.
return (0, 0) The sample tests · test_span.py
import random
from span import longest_palindrome_span
def every_substring(s):
best = (0, min(len(s), 1))
for i in range(len(s)):
for j in range(i + 1, len(s) + 1):
if s[i:j] == s[i:j][::-1] and j - i > best[1]:
best = (i, j - i)
return best
def test_examples():
"""the three examples of the prompt"""
assert longest_palindrome_span("ACGTGCAT") == (0, 7)
assert longest_palindrome_span("TTAGGATC") == (1, 6)
assert longest_palindrome_span("AGACTCT") == (0, 3)
def test_short_strings():
"""an empty string, one letter, two letters"""
assert longest_palindrome_span("") == (0, 0)
assert longest_palindrome_span("G") == (0, 1)
assert longest_palindrome_span("GG") == (0, 2)
assert longest_palindrome_span("GC") == (0, 1)
def test_no_letter_repeats_next_to_itself():
"""no palindrome longer than one letter: the first letter"""
assert longest_palindrome_span("ACGT") == (0, 1)
def test_whole_strand_and_last_letters():
"""the palindrome may be the whole strand, or reach its last letter"""
assert longest_palindrome_span("ACCA") == (0, 4)
assert longest_palindrome_span("GTACCA") == (2, 4)
def test_random_strands_against_every_substring():
"""1,500 random strands, checked against a search of every substring"""
rng = random.Random(29)
for _ in range(1500):
s = "".join(rng.choice("ACGT"[: rng.randint(1, 4)]) for _ in range(rng.randint(0, 14)))
assert longest_palindrome_span(s) == every_substring(s), f"longest_palindrome_span({s!r})"
def test_three_thousand_letters():
"""n = 3,000: fast enough only without checking every substring"""
rng = random.Random(41)
s = "".join(rng.choice("ACGT") for _ in range(3000))
s = s[:1200] + "GATTACAACATTAG" + s[1214:]
start, length = longest_palindrome_span(s)
assert length >= 14
assert s[start:start + length] == s[start:start + length][::-1] A hint
There are 2n − 1 centres: number them from 0 to 2n − 2, so that centre c starts with lo = c // 2 and hi = lo + c % 2. Widen while both ends are inside the string and match. When the loop stops, which positions are the first and the last letter of the palindrome it found?
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
3 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Centre expansion proved, in five languages, and counted Pro: opens with a Pro, Premium or Ultimate pass
Centre expansion is short, and it is correct because of two facts about centres. Here both are proved, the method runs in five languages with recorded output, and the same loop counts every palindrome.
In this part:
- Why every palindrome is found, proved with an invariant
- The code in five languages
- Counting every palindromic substring in 64 bits
3 runnable examples (C++, Go, Java, JavaScript, Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Manacher's algorithm: linear time, with counted comparisons Pro: opens with a Pro, Premium or Ultimate pass
Centre expansion repeats work on strands that repeat themselves. Manacher’s algorithm keeps what earlier centres proved, and it runs here against centre expansion on 100,000 letters, with every comparison counted.
In this part:
- The mirror rule inside the rightmost palindrome
- Why it makes at most about 4n comparisons
- Measured: 100,000 letters three ways
- When it is worth writing
1 runnable example (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Four bugs reproduced, the variants and a practice set Pro: opens with a Pro, Premium or Ultimate pass
Four one-character slips account for most wrong versions of centre expansion. The drill below hunts each of them down on short strands; the variants and a practice set come after it.
In this part:
- Four plausible bugs and the shortest strands that break them
- Variants that look the same and are not
- A practice set with sample answers
1 runnable example (Python) · about 2 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
Why does centre expansion try 2n − 1 centres on a string of n letters, and what goes wrong with only the n letters?
Every palindrome has a centre. One of odd length, such as ACGTGCA, is centred on a letter, the T in the middle; one of even length, such as TAGGAT, is centred on the gap between its two middle letters, the two Gs. A string of n letters has n letters and n − 1 gaps between neighbours, so 2n − 1 centres in all. Expanding only from the n letters finds every odd palindrome and misses every even one: on "AA" it reports a single letter instead of AA, and on TTAGGATC it finds nothing longer than one letter, missing both TT and TAGGAT. Numbering the centres 0 to 2n − 2, with lo = c // 2 and hi = lo + c % 2, handles both kinds in one loop.
-
Mid to senior
A teammate finds the longest palindromic substring of a 50,000-character log line with an n × n table of booleans, is_pal[i][j], filled by dynamic programming, and the job runs out of memory. Estimate what the table needs, and say what you would use instead and why.
-
Fresher to mid
A Java method counts the palindromic substrings of a status log with centre expansion and an int counter. On a log of 120,000 identical heartbeat characters it takes minutes and then prints a negative number. Explain both with numbers, and fix the method.
-
Senior to staff
Your centre-expansion solution for the longest palindromic substring passes every sample test but times out on a hidden test with n = 200,000. What did that test probably contain, how do you confirm it without seeing it, and what do you change?
Key takeaways
- A string of n letters has 2n − 1 centres: n letters for odd palindromes and n − 1 gaps for even ones.
- Widening a window from each centre until its ends differ finds the longest palindrome in O(n²) time and O(1) extra memory; checking every substring costs O(n³).
- The first mismatch ends a centre, because every longer window around it contains the mismatched pair.
- Keep the best span on a strictly greater length to return the palindrome that starts first.
References
- A New Linear-Time "On-Line" Algorithm for Finding the Smallest Initial Palindrome of a String (G. Manacher, Journal of the ACM 22(3), 1975) (Association for Computing Machinery)
- max() (The Python Standard Library: built-in functions) (Python Software Foundation)
- Common sequence operations: slicing (The Python Standard Library: built-in types) (Python Software Foundation)
- The Java Language Specification, Java SE 25: 4.2.1 Integral types and values (Oracle)
- String.charAt(int) (Java SE 25 API) (Oracle)
- Arithmetic operators: overflows (cppreference.com) (cppreference.com)
- The Go Programming Language Specification: index expressions (The Go Authors)
- Number.MAX_SAFE_INTEGER (MDN Web Docs)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress