Given a string s, return the number of substrings within s that are palindromes.
A palindrome is a string that reads the same forward and backward.
Example 1:
Input: s = "abc"
Output: 3Explanation: "a", "b", "c".
Example 2:
Input: s = "aaa"
Output: 6Explanation: "a", "a", "a", "aa", "aa", "aaa". Note that different substrings are counted as different palindromes even if the string contents are the same.
Constraints:
1 <= s.length <= 1000s consists of lowercase English letters.
You should aim for a solution as good or better than O(n^2) time and O(1) space, where n is the length of the given string.
A brute-force solution would be to check if every substring is a palindrome and return the total number of palindromic substrings. This would be an O(n^3) time solution. Can you think of a better way? Perhaps you should consider thinking in terms of the center of a palindrome.
Iterate over the string with index i and treat the current character as the center. For this character, try to extend outward to the left and right simultaneously, but only if both characters are equal. At each iteration, we increment the count of palindromes. How would you implement this? Can you consider both cases: even-length and odd-length palindromes?
Initialize a variable res to track the count of palindromes. At each index, create an odd-length palindrome starting at that index extending outward from both its left and right indices, i.e., i - 1 and i + 1. How can you find the even-length palindrome for this index?
For an even-length palindrome, consider expanding from indices i and i + 1. This two-pointer approach, extending from the center of the palindrome, will help find all palindromic substrings in the given string and return its count.
Before attempting this problem, you should be comfortable with:
A substring is palindromic if it reads the same forwards and backwards.
The brute-force idea is simple:
To check a palindrome, use two pointers:
If the pointers cross (or meet), the substring is a palindrome.
res = 0ijs[i : j+1]:l = i, r = jl < r and characters match:l right and r leftl >= r), it is a palindromeresresA substring s[i..j] is palindromic if:
s[i] == s[j]s[i+1..j-1] is also a palindromeSo instead of re-checking characters every time, we reuse previous results:
dp[i][j]dp[i][j] = true if substring s[i..j] is a palindromeres = 0idp[i+1][j-1] is already computed(i, j) where j >= i:s[i] == s[j] AND(j - i <= 2 OR dp[i+1][j-1] == true)dp[i][j] = trueresresEvery palindrome has a center:
Instead of checking all substrings, we:
This way, we count palindromes directly while expanding.
res = 0i in the string:l = i, r = il >= 0, r < n, and s[l] == s[r]:resl--, r++l = i, r = i + 1l >= 0, r < n, and s[l] == s[r]:resl--, r++resclass Solution:
def countSubstrings(self, s: str) -> int:
res = 0
for i in range(len(s)):
# odd length
l, r = i, i
while l >= 0 and r < len(s) and s[l] == s[r]:
res += 1
l -= 1
r += 1
# even length
l, r = i, i + 1
while l >= 0 and r < len(s) and s[l] == s[r]:
res += 1
l -= 1
r += 1
return resEvery palindromic substring can be identified by expanding from its center.
There are only two possible centers for any palindrome:
Instead of duplicating logic for both cases, we extract the expansion logic into a helper function (countPali).
This keeps the solution clean, reusable, and optimal.
For each index i, we:
(i, i)(i, i + 1)Each successful expansion corresponds to one valid palindrome.
res = 0i in the string:countPali(s, i, i)countPali(s, i, i + 1)countPali(s, l, r):l >= 0, r < n, and s[l] == s[r]:resl--, r++class Solution:
def countSubstrings(self, s: str) -> int:
res = 0
for i in range(len(s)):
res += self.countPali(s, i, i)
res += self.countPali(s, i, i + 1)
return res
def countPali(self, s, l, r):
res = 0
while l >= 0 and r < len(s) and s[l] == s[r]:
res += 1
l -= 1
r += 1
return resThe “expand around center” idea is great, but it can redo the same comparisons many times, making it O(n^2).
Manacher’s Algorithm speeds this up to O(n) by using two tricks:
Normalize odd/even palindromes
# between characters:"abba" → "#a#b#b#a#"Reuse information using a “current best palindrome window”
[l, r] (the farthest-reaching palindrome found so far).i inside [l, r], we know its mirror position mirror = l + (r - i).i can be at least the smaller of:r - i)mirror (p[mirror])This avoids repeated expansions and ensures total work is linear.
t by inserting # between characters and at ends.p where p[i] = radius of palindrome centered at i in t.[l, r].i:i is inside [l, r], initialize p[i] using its mirror.i extends beyond r, update [l, r].p into the number of palindromic substrings in the original string:(p[i] + 1) // 2res.class Solution:
def countSubstrings(self, s: str) -> int:
def manacher(s):
t = '#' + '#'.join(s) + '#'
n = len(t)
p = [0] * n
l, r = 0, 0
for i in range(n):
p[i] = min(r - i, p[l + (r - i)]) if i < r else 0
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:
l, r = i - p[i], i + p[i]
return p
p = manacher(s)
res = 0
for i in p:
res += (i + 1) // 2
return resWhen using the expand-around-center approach, many developers only expand from single characters (odd-length centers). Even-length palindromes like "aa" or "abba" require expanding from between two characters, so both (i, i) and (i, i+1) centers must be checked.
In the DP approach, the recurrence dp[i][j] = dp[i+1][j-1] requires careful iteration order. Processing i from high to low ensures dp[i+1][j-1] is computed before dp[i][j]. Iterating in the wrong direction leads to reading uninitialized values.
Some implementations accidentally count the same palindrome multiple times or miss counting single characters as palindromes. Every single character is itself a palindrome, so the minimum count for a string of length n is n.