title: Longest Palindromic Substring
slug: longest-palindromic-substring
difficulty: medium
leetcode_id: 5
leetcode_url: https://leetcode.com/problems/longest-palindromic-substring/
categories:
  - strings
  - dynamic-programming
patterns:
  - slug: two-pointers
    is_optimal: true
  - slug: dynamic-programming
    is_optimal: false

function_signature: "def longest_palindrome(s: str) -> str:"

test_cases:
  visible:
    - input: { s: "babad" }
      expected: "bab"
    - input: { s: "cbbd" }
      expected: "bb"
  hidden:
    - input: { s: "a" }
      expected: "a"
    - input: { s: "ac" }
      expected: "a"
    - input: { s: "racecar" }
      expected: "racecar"
    - input: { s: "aacabdkacaa" }
      expected: "aca"

description: |
  Given a string `s`, return *the longest palindromic substring* in `s`.

  A **palindrome** is a string that reads the same forward and backward.

constraints: |
  - `1 <= s.length <= 1000`
  - `s` consists of only digits and English letters

examples:
  - input: 's = "babad"'
    output: '"bab"'
    explanation: '"aba" is also a valid answer — both have length 3.'
  - input: 's = "cbbd"'
    output: '"bb"'
    explanation: "The longest palindromic substring is \"bb\" with length 2."

explanation:
  intuition: |
    Every palindrome has a **center**. For odd-length palindromes like "aba", the center is the middle character 'b'. For even-length palindromes like "abba", the center is the gap between the two 'b's.

    Think of it like this: if we know the center, we can find the full palindrome by **expanding outward** — checking if the characters on both sides match. We keep expanding until they don't match.

    The strategy is simple: try every possible center (each character and each gap between characters), expand to find the longest palindrome for that center, and track the overall longest.

    This "expand around center" approach is intuitive and uses O(1) extra space, making it ideal for interviews.

  approach: |
    We solve this using **Expand Around Center**:

    **Step 1: Define the expand helper function**

    - `expand(left, right)` returns the bounds of the longest palindrome centered at this position
    - While `left >= 0` and `right < len(s)` and `s[left] == s[right]`:
      - Expand: decrement `left`, increment `right`
    - Return the bounds of the palindrome (after adjusting for the last failed expansion)

    &nbsp;

    **Step 2: Try every possible center**

    - For each index `i`:
      - Try **odd-length** palindrome: `expand(i, i)` — center is single character
      - Try **even-length** palindrome: `expand(i, i+1)` — center is between characters
      - Update the best result if either expansion found a longer palindrome

    &nbsp;

    **Step 3: Return the longest palindrome**

    - Track `start` and `end` indices of the best palindrome found
    - Return `s[start:end+1]`

    &nbsp;

    Why does this work? By checking both odd and even centers at every position, we're guaranteed to find the center of the longest palindrome somewhere.

  common_pitfalls:
    - title: Forgetting Even-Length Palindromes
      description: |
        If you only expand around single characters, you'll miss even-length palindromes like "abba" or "bb".

        Every position needs two expansion attempts: one for odd (center at `i`) and one for even (center between `i` and `i+1`).
      wrong_approach: "Only calling expand(i, i)"
      correct_approach: "Call both expand(i, i) and expand(i, i+1)"

    - title: Index Out of Bounds During Expansion
      description: |
        The expansion loop must check bounds **before** accessing characters. A common mistake is checking equality first, which causes an index error.
      wrong_approach: "while s[left] == s[right] and left >= 0 and right < len(s)"
      correct_approach: "while left >= 0 and right < len(s) and s[left] == s[right]"

    - title: Returning Length Instead of Substring
      description: |
        The problem asks for the actual substring, not just its length. Track the start and end positions of the best palindrome so you can extract it.
      wrong_approach: "return max_length"
      correct_approach: "return s[start:end+1]"

  key_takeaways:
    - "**Expand around center**: O(n²) time, O(1) space — optimal for interviews"
    - "**Handle both odd and even**: Check single-character centers AND gaps between characters"
    - "**Track positions, not just length**: You need to return the actual substring"
    - "**Manacher's algorithm**: Can solve in O(n) but is complex — not expected in interviews"

  time_complexity: "O(n²). For each of n possible centers, expansion can take up to O(n) time in the worst case."
  space_complexity: "O(1). Only a few variables for tracking positions — no additional data structures."

solutions:
  - approach_name: Expand Around Center
    is_optimal: true
    code: |
      def longest_palindrome(s: str) -> str:
          def expand(left: int, right: int) -> tuple[int, int]:
              """Expand around center and return palindrome bounds."""
              while left >= 0 and right < len(s) and s[left] == s[right]:
                  left -= 1
                  right += 1
              # Return bounds of palindrome (undo last expansion)
              return left + 1, right - 1

          start, end = 0, 0

          for i in range(len(s)):
              # Try odd-length palindrome (single character center)
              l1, r1 = expand(i, i)
              if r1 - l1 > end - start:
                  start, end = l1, r1

              # Try even-length palindrome (center between characters)
              l2, r2 = expand(i, i + 1)
              if r2 - l2 > end - start:
                  start, end = l2, r2

          return s[start:end + 1]
    explanation: |
      **Time Complexity:** O(n²) — n centers, up to n expansion steps each.

      **Space Complexity:** O(1) — Only tracking start/end positions.

      For each position, we try both odd and even palindrome centers. The expand function returns the bounds of the longest palindrome for that center. We track the overall longest and return it at the end.

  - approach_name: Dynamic Programming
    is_optimal: false
    code: |
      def longest_palindrome(s: str) -> str:
          n = len(s)
          if n < 2:
              return s

          # dp[i][j] = True if s[i:j+1] is a palindrome
          dp = [[False] * n for _ in range(n)]
          start, max_len = 0, 1

          # Single characters are palindromes
          for i in range(n):
              dp[i][i] = True

          # Check substrings of increasing length
          for length in range(2, n + 1):
              for i in range(n - length + 1):
                  j = i + length - 1

                  if length == 2:
                      # Two characters: palindrome if they match
                      dp[i][j] = s[i] == s[j]
                  else:
                      # Longer: palindrome if ends match AND inner is palindrome
                      dp[i][j] = s[i] == s[j] and dp[i + 1][j - 1]

                  if dp[i][j] and length > max_len:
                      start, max_len = i, length

          return s[start:start + max_len]
    explanation: |
      **Time Complexity:** O(n²) — Fill n×n table.

      **Space Complexity:** O(n²) — DP table storage.

      Build up palindrome information from smaller to larger substrings. `dp[i][j]` is True if substring from i to j is a palindrome. A substring is a palindrome if its ends match and its inner substring is also a palindrome. Track the longest one found.
