title: Palindromic Substrings
slug: palindromic-substrings
difficulty: medium
leetcode_id: 647
leetcode_url: https://leetcode.com/problems/palindromic-substrings/
categories:
  - strings
  - dynamic-programming
patterns:
  - two-pointers
  - dynamic-programming

function_signature: "def count_substrings(s: str) -> int:"

test_cases:
  visible:
    - input: { s: "abc" }
      expected: 3
    - input: { s: "aaa" }
      expected: 6
  hidden:
    - input: { s: "a" }
      expected: 1
    - input: { s: "aa" }
      expected: 3
    - input: { s: "ab" }
      expected: 2
    - input: { s: "aba" }
      expected: 4
    - input: { s: "abba" }
      expected: 6

description: |
  Given a string `s`, return *the number of **palindromic substrings** in it*.

  A string is a **palindrome** when it reads the same backward as forward.

  A **substring** is a contiguous sequence of characters within the string.

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

examples:
  - input: 's = "abc"'
    output: "3"
    explanation: "Three palindromic strings: \"a\", \"b\", \"c\"."
  - input: 's = "aaa"'
    output: "6"
    explanation: 'Six palindromic strings: "a", "a", "a", "aa", "aa", "aaa".'

explanation:
  intuition: |
    Imagine each character in the string as a potential **centre** of a palindrome. A palindrome is symmetric — it mirrors around its centre.

    The key insight is that palindromes can have either:
    - **Odd length**: A single character at the centre (e.g., "aba" centres on "b")
    - **Even length**: Two identical characters at the centre (e.g., "abba" centres between the two "b"s)

    Think of it like dropping a pebble into still water and watching ripples expand outward. From each centre point, we **expand outward** in both directions as long as the characters on both sides match. Each successful expansion represents another valid palindrome we've found.

    By systematically considering every possible centre (both single characters and pairs of adjacent characters), we can count all palindromic substrings without missing any or counting duplicates.

  approach: |
    We solve this using the **Expand Around Centre** approach:

    **Step 1: Initialise a counter**

    - `count`: Set to `0` to track the total number of palindromic substrings

    &nbsp;

    **Step 2: Iterate through each potential centre**

    - For each index `i` from `0` to `n-1`, treat it as a potential centre
    - Each position can be the centre of both odd-length and even-length palindromes

    &nbsp;

    **Step 3: Expand around odd-length centres**

    - Call `expand(s, i, i)` — starting with a single character centre
    - Expand outward while `s[left] == s[right]`
    - Each successful comparison means we found another palindrome
    - Continue until characters don't match or we hit boundaries

    &nbsp;

    **Step 4: Expand around even-length centres**

    - Call `expand(s, i, i + 1)` — starting with two adjacent characters as the centre
    - Same expansion logic as odd-length centres
    - This catches palindromes like "aa", "abba", etc.

    &nbsp;

    **Step 5: Return the total count**

    - Return `count` after processing all centres

    &nbsp;

    This approach is efficient because each expansion takes O(n) time in the worst case, and we have O(n) centres, giving us O(n²) total — much better than checking all O(n²) substrings explicitly.

  common_pitfalls:
    - title: Checking All Substrings Naively
      description: |
        A common first approach is to generate all substrings and check each one for being a palindrome:
        - Outer loop for start index: O(n)
        - Inner loop for end index: O(n)
        - Palindrome check for each substring: O(n)

        This results in **O(n³) time complexity**. While it passes for `n <= 1000`, it's significantly slower than necessary. For larger inputs (in similar problems), this approach would cause TLE.
      wrong_approach: "Generate all substrings, check each for palindrome"
      correct_approach: "Expand around centres in O(n²)"

    - title: Forgetting Even-Length Palindromes
      description: |
        When expanding from centres, it's easy to only consider single characters as centres (odd-length palindromes).

        For example, in "abba", if you only expand from single characters, you'll find "a", "b", "b", "a" but miss "bb" and "abba".

        You must expand from both:
        - Single characters: `expand(i, i)` for odd-length
        - Adjacent pairs: `expand(i, i+1)` for even-length
      wrong_approach: "Only expand from single character centres"
      correct_approach: "Expand from both single characters and adjacent pairs"

    - title: Off-by-One Errors in Expansion
      description: |
        When expanding outward, boundary checks are crucial. The expansion should stop when:
        - `left < 0` (hit the start of string)
        - `right >= n` (hit the end of string)
        - `s[left] != s[right]` (characters don't match)

        A common mistake is checking boundaries after accessing the characters, causing index out of bounds errors.
      wrong_approach: "Check boundaries after character comparison"
      correct_approach: "Check boundaries before accessing characters"

  key_takeaways:
    - "**Expand around centre pattern**: For palindrome problems, thinking in terms of centres rather than endpoints often leads to cleaner solutions"
    - "**Odd vs even length**: Always consider both cases when dealing with palindromes — single character centres and two-character centres"
    - "**Foundation for Longest Palindromic Substring**: The same expand-around-centre technique solves LeetCode 5, just track the longest instead of counting"
    - "**Alternative: Dynamic Programming**: This problem can also be solved with DP where `dp[i][j]` indicates if `s[i:j+1]` is a palindrome, useful for problems requiring the actual substrings"

  time_complexity: "O(n²). For each of the `n` positions, we potentially expand up to `n` times in each direction."
  space_complexity: "O(1). We only use a constant number of variables for counting and expansion indices."

solutions:
  - approach_name: Expand Around Centre
    is_optimal: true
    code: |
      def count_substrings(s: str) -> int:
          def expand(left: int, right: int) -> int:
              """Count palindromes by expanding from centre."""
              count = 0
              # Expand while within bounds and characters match
              while left >= 0 and right < len(s) and s[left] == s[right]:
                  count += 1  # Found a palindrome
                  left -= 1   # Expand left
                  right += 1  # Expand right
              return count

          total = 0
          for i in range(len(s)):
              # Odd-length palindromes (single character centre)
              total += expand(i, i)
              # Even-length palindromes (two character centre)
              total += expand(i, i + 1)

          return total
    explanation: |
      **Time Complexity:** O(n²) — For each of n centres, expansion can take up to O(n) time.

      **Space Complexity:** O(1) — Only using a few integer variables.

      We iterate through each position as a potential centre and expand outward for both odd and even length palindromes. Each expansion continues while characters match, counting each valid palindrome found.

  - approach_name: Dynamic Programming
    is_optimal: false
    code: |
      def count_substrings(s: str) -> int:
          n = len(s)
          count = 0

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

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

          # Check substrings of length 2
          for i in range(n - 1):
              if s[i] == s[i + 1]:
                  dp[i][i + 1] = True
                  count += 1

          # Check substrings of length 3 and above
          for length in range(3, n + 1):
              for i in range(n - length + 1):
                  j = i + length - 1
                  # Palindrome if ends match and middle is palindrome
                  if s[i] == s[j] and dp[i + 1][j - 1]:
                      dp[i][j] = True
                      count += 1

          return count
    explanation: |
      **Time Complexity:** O(n²) — We fill an n×n DP table.

      **Space Complexity:** O(n²) — We store the DP table.

      This approach builds up from smaller substrings: single characters are palindromes, length-2 substrings are palindromes if both characters match, and longer substrings are palindromes if the ends match and the middle (already computed) is a palindrome. While correct and useful for understanding, the expand-around-centre approach is preferred due to O(1) space.

  - approach_name: Brute Force
    is_optimal: false
    code: |
      def count_substrings(s: str) -> int:
          def is_palindrome(substring: str) -> bool:
              """Check if a string is a palindrome."""
              return substring == substring[::-1]

          count = 0
          n = len(s)

          # Check all possible substrings
          for i in range(n):
              for j in range(i, n):
                  if is_palindrome(s[i:j + 1]):
                      count += 1

          return count
    explanation: |
      **Time Complexity:** O(n³) — O(n²) substrings, each taking O(n) to check.

      **Space Complexity:** O(n) — Creating substring copies for comparison.

      This approach explicitly generates every substring and checks if it's a palindrome by comparing it to its reverse. While straightforward and correct, it's significantly slower than the optimal approaches. Included to illustrate the progression from naive to optimal thinking.
