leetcode / 1-D dynamic programming / Palindromes core

Palindromes

A palindrome is a string that reads the same forwards and backwards, and it has a neat recursive shape: wrap the same letter around a smaller palindrome. That gives two ways to find them all: grow outward from each possible centre, or fill a table of which substrings are palindromes from short to long.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. Expand around centres
  5. The substring table
  6. Manacher's algorithm
  7. Invariant
  8. Worked example
  9. Implementation
  10. Variations and edge cases
  11. Complexity and tradeoffs
  12. Problems
  13. Medium
  14. Also uses this pattern
  15. Connections
  16. Read first
  17. Read next
  18. Check yourself
  19. References

Problem

You are asked about palindromic substrings (contiguous pieces) of a string: the longest one, how many there are, or a way to cut the string into palindromes. Checking every substring by reversing it costs O(n) each, and there are O(n²) substrings, so O(n³) in all. Two observations cut that down: a palindrome is determined by its centre and radius, and a substring is a palindrome exactly when its ends match and the inside is a palindrome.

SignalPatternWhy
"Longest palindromic substring"Expand around each of the 2n - 1 centresEvery palindrome has one centre, and growing outward stops at the first mismatch.
"Count palindromic substrings"Same expansion, count every step that matchesEach successful step is a distinct palindrome with that centre.
Many queries "is s[i..j] a palindrome?"Boolean table over (i, j)The answer for (i, j) needs only (i + 1, j - 1), a shorter span.
"Cut into palindromes with fewest cuts" or "list all cuts"Palindrome table plus a second DP or backtrackingThe table answers "is this piece allowed?" in O(1) for the outer search.
Linear-time longest palindromeManacher's algorithmReuses the mirror image of already-known palindromes inside the current widest one.

Intuition

Hold a palindrome up to a mirror at its middle: the left half matches the right half. So if you stand at the middle and step outward one letter on each side at a time, the palindrome keeps growing exactly as long as the two letters you touch are equal. The first mismatch ends it. A string of length n has n centres on letters (odd-length palindromes) and n - 1 centres between letters (even-length ones), so 2n - 1 places to try.

The other view is a table. Whether "abba" is a palindrome depends on its two end letters matching and on "bb" being a palindrome. "bb" in turn depends on its ends and the empty string in between. Every substring depends on a shorter one, so filling the table in order of length works.

Which substrings of "cabbad" are palindromes A table with the start index as the row and the end index as the column. Cells at or right of the diagonal are filled. 8 substrings are palindromes and the longest is "abba". c 0 c 0 a 1 a 1 b 2 b 2 b 3 b 3 a 4 a 4 d 5 d 5 T T T T T T T T

Row is where the substring starts, column is where it ends. Green cells are palindromes, and the blue cell is the longest. The diagram scrolls sideways on a narrow screen.

Mechanics

Expand around centres

For each of the 2n - 1 centres, set two pointers: both on the same letter for an odd centre, or on neighbours for an even one. While both are in range and the letters match, record the palindrome, then move the left pointer one step left and the right one step right. When the loop stops, the last palindrome seen is the widest at that centre. The longest palindrome overall is the widest of these, and the count is the total number of successful steps.

The invariant: whenever the pointers are at (l, r) and the loop condition has just held, s[l..r] is a palindrome. It holds at the start (a single letter, or an empty gap that counts as a palindrome of length zero) and each step adds matching letters around a palindrome. The cost is O(n) per centre in the worst case, such as a string of one repeated letter, O(n²) in total, with O(1) extra space.

Widest palindrome at each centre of "cabbad" For every character and every gap in "cabbad", the widest palindrome around it: char 0 gives "c"; gap 0|1 gives nothing; char 1 gives "a"; gap 1|2 gives nothing; char 2 gives "b"; gap 2|3 gives "abba"; char 3 gives "b"; gap 3|4 gives nothing; char 4 gives "a"; gap 4|5 gives nothing; char 5 gives "d". The longest is "abba" and 8 palindromic substrings are found in all. c a b b a d char 0 c gap 0|1 char 1 a gap 1|2 char 2 b gap 2|3 abba char 3 b gap 3|4 char 4 a gap 4|5 char 5 d

Each bar is the widest palindrome around that centre. The gap between the two b's grows from "bb" to the blue "abba".

The substring table

Define is_pal[i][j] as true when s[i..j] is a palindrome. Then is_pal[i][j] = (s[i] == s[j]) and (j - i < 2 or is_pal[i + 1][j - 1]). Spans of length 1 are always palindromes; spans of length 2 are palindromes when the two letters match; longer spans need their inner span to be one. Fill by increasing length (or with i descending), so the inner entry already exists. This costs O(n²) time and space, and it makes every later "is this piece a palindrome?" question O(1).

Manacher's algorithm

Put a separator between every pair of letters (and at both ends) so every palindrome has odd length in the padded string. Let radii[k] be how far the palindrome centred at k extends. Keep the centre and right edge of the palindrome that reaches furthest right. A new position k inside it has a mirror position on the other side, and the palindrome there already tells you a lower bound for k. Only positions that push the right edge further need new character comparisons, so the total work is O(n). For "cabbad" the padded string is #c#a#b#b#a#d# and the radii are 0, 1, 0, 1, 0, 1, 4, 1, 0, 1, 0, 1, 0; the largest radius, 4, is the length of "abba".

Invariant

The table is correct by induction on length: a string is a palindrome iff its first and last letters match and the part between them is a palindrome, and the part between is shorter. The expansion is correct because palindromes with the same centre are nested: if the radius-r palindrome exists, so does every smaller one, so the first mismatch rules out all larger.

Worked example

Take the string "cabbad". Expanding from the 11 centres finds the widest palindrome at each, as in the diagram, and the winner is "abba", starting at index 1. Counting every step that matched gives 8 palindromic substrings: 6 single letters, "bb" and "abba".

CentreStartWidest palindrome
char 00"c"
gap 0|11none
char 11"a"
gap 1|22none
char 22"b"
gap 2|31"abba"
char 33"b"
gap 3|44none
char 44"a"
gap 4|55none
char 55"d"

The table agrees: it has 8 true cells and the longest is "abba". The cell for "abba" (start 1, end 4) is true because a == a and the inner cell (start 2, end 3), "bb", is true. Manacher's radii give the same longest string, "abba", from the centre with radius 4.

Implementation

def longest_palindrome(s):
    best = ""
    for center in range(2 * len(s) - 1):
        left = center // 2
        right = left + center % 2
        while left >= 0 and right < len(s) and s[left] == s[right]:
            left -= 1
            right += 1
        if right - left - 1 > len(best):
            best = s[left + 1:right]
    return best


def count_palindromes(s):
    count = 0
    for center in range(2 * len(s) - 1):
        left = center // 2
        right = left + center % 2
        while left >= 0 and right < len(s) and s[left] == s[right]:
            count += 1
            left -= 1
            right += 1
    return count

Variations and edge cases

def palindrome_table(s):
    n = len(s)
    is_pal = [[False] * n for _ in range(n)]
    for i in range(n - 1, -1, -1):
        for j in range(i, n):
            is_pal[i][j] = s[i] == s[j] and (j - i < 2 or is_pal[i + 1][j - 1])
    return is_pal


def min_cuts(s):
    is_pal = palindrome_table(s)
    cuts = list(range(-1, len(s)))        # cuts[i]: fewest cuts for s[:i]
    for end in range(1, len(s) + 1):
        for start in range(end):
            if is_pal[start][end - 1]:
                cuts[end] = min(cuts[end], cuts[start] + 1)
    return cuts[-1]


def manacher(s):
    t = "#" + "#".join(s) + "#"
    radii = [0] * len(t)
    center = right = 0
    for k in range(len(t)):
        if k < right:
            radii[k] = min(right - k, radii[2 * center - k])
        while (k - radii[k] - 1 >= 0 and k + radii[k] + 1 < len(t)
               and t[k - radii[k] - 1] == t[k + radii[k] + 1]):
            radii[k] += 1
        if k + radii[k] > right:
            center, right = k, k + radii[k]
    return radii
  • Even centres. Forgetting the n - 1 gaps misses every even-length palindrome such as "abba". Starting both pointers at the same index only finds odd ones.
  • Slicing the result. After the loop the pointers have stepped one past the palindrome, so the slice is s[left + 1:right].
  • Table order. Iterating i ascending reads is_pal[i + 1][j - 1] before it is written. Go down in i or up in length.
  • Empty and one-letter strings. The empty string has no centres; a single letter is its own answer. Return an empty string explicitly if needed.
  • Case and punctuation. Questions about phrases clean the text first, then use two pointers; that is the two-pointers page, not this one.

Complexity and tradeoffs

  • Time: centre expansion is O(n²) worst case and usually much faster on real text. The table is always O(n²). Manacher is O(n).
  • Space: O(1) for expansion, O(n²) for the table, O(n) for Manacher.
  • Baseline. Reversing every substring is O(n³).
  • Which to use. For one question about the longest or the count, expansion is the shortest correct code. Use the table when later steps need constant-time palindrome checks, as in minimum cuts or partitioning. Manacher is the answer to "can you do it in linear time", and rarely required.
  • When it fails. Subsequence questions (letters need not be adjacent) use a different table, over two ends of the string, on the string DP page. Palindromes under edits are an edit-distance variant.

Problems

Longest Palindromic Substring and Palindromic Substrings are the two textbook uses of centre expansion. Palindrome Partitioning and Palindrome Pairs use the same palindrome check inside a larger search.

Medium

  • 5. Longest Palindromic Substring Blind 75 Grind 169 NeetCode 150 NeetCode 250
    Every palindrome has a center, so expanding outward from each of the 2n-1 centers finds the longest one in quadratic time with constant space.
  • 647. Palindromic Substrings Blind 75 NeetCode 150 NeetCode 250
    Expand around each of the 2n-1 centres and count every expansion that is still a palindrome.

Also uses this pattern

These problems have a different home page, but this idea is part of their solution.

Connections

Read first

Read next

No page builds on this one yet.

Centre expansion is two pointers moving outward instead of inward. The table is a first example of DP over a pair of indices, which grid DP and two-string DP then generalise. Palindrome Partitioning combines the table with backtracking.

Check yourself

Why does a string of length n have 2n - 1 centres?

Odd-length palindromes centre on a letter (n choices) and even-length ones centre between two letters (n - 1 choices). For "cabbad" that is 11.

Why can expansion stop at the first mismatch?

Palindromes with one centre are nested: a wider one contains every narrower one. If radius r fails, no larger radius can succeed. At the gap between the two b's, expansion reaches "abba" and stops.

In the table, why must the loop go over i from high to low?

Entry (i, j) reads (i + 1, j - 1). With i descending, row i + 1 is complete before row i starts. The cell for "abba" reads the cell for "bb".

How many palindromic substrings does "cabbad" have, and what is the longest?

8: 6 single letters, "bb" and "abba". The longest is "abba".

References

  • Manacher, G. K., "A New Linear-Time On-Line Algorithm for Finding the Smallest Initial Palindrome of a String", Journal of the ACM 22(3), 1975.
  • Cormen, Leiserson, Rivest and Stein, Introduction to Algorithms, chapter 14, the longest palindromic subsequence exercise.
  • Gusfield, Algorithms on Strings, Trees, and Sequences, Cambridge University Press, 1997, section 9.2 (finding palindromes).
  • Sedgewick and Wayne, Algorithms, section 5.3 (substring search): linear-time scanning that, like Manacher's algorithm, reuses earlier comparisons instead of restarting.
  • Skiena, The Algorithm Design Manual, chapter 10 (dynamic programming).
  • The NeetCode roadmap and the Blind 75, Grind 169, NeetCode 150 and NeetCode 250 lists, used as the index of topics and problems.