DSA sheet · Strings · Two Pointers pattern
Palindromic Substrings
This is the last question of the two-pointers strings section. It's a close cousin of Longest Palindromic Substring (Problem 9): there we wanted the longest palindrome, here we count all of them. The teacher reuses the same two steps: a brute force that makes every substring and checks it with two pointers (about n³), then expand around center, where every successful step outwards is one more palindrome to count (n²). She also clears up a common confusion: two pointers is not only for sorted arrays. You can use it whenever you know which pointer to move and when.
Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember
- Part 0 · Two pointers from scratch (what this page needs)
- Part A · Brute force: count every substring that passes the check
- Part B · Optimal: expand around every center and count each step
- Part C · Revision page
Part 0 · Before starting
Words used on this page
- Index: the position of a character, counted from 0.
- Substring: a continuous piece of the string, with no characters skipped. The same letters at different positions count as different substrings. In "aaa", index 0 alone and index 1 alone are two separate substrings, even though both are "a".
- Palindrome: reads the same forwards and backwards ("a", "aa", "aba", "abba"). Every single letter is one.
- Pointer: an integer variable that holds an index.
What "two pointers" means
Looking at every pair of positions with two nested loops costs about n²/2 pairs, so O(n²). With two pointers you keep just two indexes and move them by a rule, so some pairs are skipped safely. People often say "two pointers needs a sorted array". The teacher corrects this: sorting is just the usual reason we know which pointer to move. If the problem gives you that knowledge some other way (like matching letters in a palindrome), two pointers works on unsorted data too.
| shape | how the pointers move | example |
|---|---|---|
| Opposite ends | left at 0, right at n−1, walking towards each other. On sorted data, a sum that's too small moves left up and one that's too big moves right down. The other move could only make it worse, so no answer is skipped. | pair sum, Container With Most Water |
| Same direction | slow "write" and fast "read" pointers both go left to right. | Move Zeroes |
| Three pointers | low / mid / high split the array into the 0s, 1s, unknown part and 2s. | Sort Colors |
| Fix one + two pointers | a loop fixes one value, and two pointers search the rest, skipping duplicates. | 3Sum |
| left-max / right-max | opposite ends, each remembering the tallest bar seen. The smaller side moves. | Trapping Rain Water |
| Both ends of a string | compare s[left] and s[right], then move both inwards. | Valid Palindrome, Part A here |
| Expand around a center | start in the middle, move both outwards while the letters match. | Part B here, Problem 9 |
Checking one palindrome with two pointers (shrinking)
Reading the same both ways means 1st letter = last, 2nd = 2nd-last, and so on. So set left at the start and right at the end. A mismatch means "not a palindrome". A match means both move one step inwards. Stop when they meet or cross. That's about n/2 comparisons, and it needs no reversed copy.
Expand around a center, and why there are 2n − 1 centers
Every palindrome mirrors around its middle. Start at the middle and step outwards. If the two new outer letters match, the bigger piece is a palindrome too. Stop at the first mismatch or at the edge of the string.
- Odd length ("a", "aba"): the middle is a letter →
left = right = i. - Even length ("aa", "abba"): the middle is the gap between two letters →
left = i, right = i + 1.
n letters have n − 1 gaps between them, so there are n + (n − 1) = 2n − 1 possible centers. Every palindromic substring has exactly one center and one size. So if we visit every center and record every size it reaches, we meet each palindrome exactly once. That's what makes counting work.
s: a a a
index: 0 1 2
centers: o e o e o o = letter (odd), e = gap (even)
3 odd + 2 even = 5 = 2·3 − 1
Part A · Brute force: count every substring that passes the check
LeetCode 647
1The question in simple words
You get a string s. Return how many of its substrings are palindromes. Substrings at different positions are counted separately, even if they have the same letters.
The teacher's example, s = "aaa":
| substring | indexes | palindrome? |
|---|---|---|
| a | 0..0 | yes |
| a | 1..1 | yes |
| a | 2..2 | yes |
| aa | 0..1 | yes |
| aa | 1..2 | yes |
| aaa | 0..2 | yes |
All 6 substrings are palindromes → answer 6. Another example (LeetCode's first): "abc" → only the three single letters → answer 3.
2What the constraints tell us
- 1 ≤ s.length ≤ 1000 → n ≤ 10³, and the string is never empty (the answer is at least 1).
- She reads the constraints to see which pattern and which complexity will fit. n² = 10⁶ is easy. n³ = 10⁹ is past the usual ~10⁸ budget for simple operations, so it risks TLE.
- Only lowercase English letters.
3Intuition: it's Problem 9 with a counter instead of a "best"
In Longest Palindromic Substring we made every substring, checked it, and if it was a palindrome we asked "is it the longest so far?". Here the question after the check is simpler: if it's a palindrome, add 1. So the plan is:
- Make every substring with a start
iand an endj(j goes from i to the end). - Check each one with the shrinking two-pointer check.
- Every time the check says yes,
count += 1.
4Building the logic from examples
Generating the substrings of "aaa"
Fix i, then move j from i to the end: i = 0 gives "a", "aa", "aaa"; i = 1 gives "a", "aa"; i = 2 gives "a". That's 6 = n(n+1)/2 substrings.
Two ways to give the substring to the check
The teacher shows both:
- Build it: keep a string
subfor each i. Every time j moves, adds[j]to the end. Thensubis exactly s[i..j]: "a", then "aa", then "aaa". Passsubto the check. - Don't build it, pass the indexes: give the check the whole string plus
iandjas left and right. The check only moves inwards from those two positions, so it never looks outside s[i..j]. Her example: in "ababc", to check the middle "bab" (indexes 1..3), send left = 1, right = 3. It compares b with b, then meets in the middle, and never touches index 0 or 4.
→ Passing indexes. Building
sub makes a new string every time j moves (strings can't be changed in place in Python or Java), which costs extra time and memory. Both give the same count, and both are O(n³) overall. The page's main code uses indexes. The "build it" version is shown below too, and the test file checks both.→ Sorting is useful because it tells you which pointer to move. Here we know that without sorting: if
s[left] == s[right], the first and last letters are done, so the next pair to check is the 2nd and 2nd-last, and both move inwards. If they differ, we stop. Knowing the move rule is the real requirement.5Approach steps
count = 0.- For every start
i, for every endjfrom i to n−1: - If
isPalindrome(s, i, j)(shrinking two pointers), docount += 1. - Return
count.
6Code (Python)
class Solution:
def countSubstrings(self, s):
count = 0
n = len(s)
for i in range(n): # start of the substring
for j in range(i, n): # end, begins at i
if self.isPalindrome(s, i, j):
count += 1 # one more palindrome
return count
def isPalindrome(self, s, left, right):
while left < right: # until they meet or cross
if s[left] != s[right]:
return False
left += 1 # shrink inwards
right -= 1
return Trueclass Solution:
def countSubstrings(self, s):
count = 0
for i in range(len(s)):
sub = "" # fresh substring for each start
for j in range(i, len(s)):
sub += s[j] # sub is now s[i..j]
if self.isPalindrome(sub, 0, len(sub) - 1):
count += 1
return count
def isPalindrome(self, s, left, right):
while left < right:
if s[left] != s[right]:
return False
left += 1
right -= 1
return True7Code line by line
| line | what it means |
|---|---|
| count = 0 | How many palindromes we've found. |
| for i in range(n): for j in range(i, n): | Every (start, end) pair with start ≤ end means every substring once. |
| if self.isPalindrome(s, i, j): | Check only s[i..j], using i and j as the two pointers. |
| count += 1 | No "is it the best" question here. Every palindrome simply counts. |
| while left < right: | Keep comparing pairs until the pointers meet or cross. |
| if s[left] != s[right]: return False | One mismatched pair is enough to say no. |
| left += 1; right -= 1 | The pair matched. Move to the next pair inwards. |
| sub += s[j] | (Second version) grow the substring by one letter, so it always equals s[i..j]. |
8Dry run (hand table)
s = "aaa".
| i | j | s[i..j] | pairs compared (left, right) | palindrome? | count after |
|---|---|---|---|---|---|
| 0 | 0 | a | none (left = right) | yes | 1 |
| 0 | 1 | aa | (0,1) a=a → cross | yes | 2 |
| 0 | 2 | aaa | (0,2) a=a → meet at 1 | yes | 3 |
| 1 | 1 | a | none | yes | 4 |
| 1 | 2 | aa | (1,2) a=a → cross | yes | 5 |
| 2 | 2 | a | none | yes | 6 |
And s = "abc" for contrast: (0,0) ✓ 1 · (0,1) a≠b ✗ · (0,2) a≠c ✗ · (1,1) ✓ 2 · (1,2) b≠c ✗ · (2,2) ✓ 3 → 3.
Final answer for "aaa": 6 ✓
9Complexity & remember
- Time O(n³): n² substrings from the two loops, times about n/2 for each check → about n³/2. The teacher's numbers for n = 1000: 10⁶ substrings × 500 comparisons = 5 × 10⁸.
- Is that too slow? Past 10⁸ you can get TLE, but it depends on how heavy each step is. Here each step is one character comparison, which is very light, so around 5–6 × 10⁸ can still pass. It was accepted when she submitted, but slowly. (Sometimes even 10⁷–10⁸ can TLE if each step does a lot of work, so this is a judgement call, not a rule.)
- Space O(1) for the index version. The "build sub" version also keeps an O(n) string.
- The check can't get cheaper than n/2. The part to cut is the two loops that generate substrings, exactly as in Problem 9.
for i, for j from i, if isPalindrome(s, i, j): count += 1.Pass indexes rather than building strings. ≈ n³/2, it passes for n = 1000 but slowly.
Part B · Optimal: expand around every center and count each step
1The question again, with the new goal
Count the palindromic substrings again, but with one loop instead of two loops for generating substrings. The aim is O(n²).
2What the constraints tell us
- n ≤ 1000 → n² = 10⁶ operations, far below 10⁸. O(n²) is comfortable.
- n ≥ 1, so the loop always runs at least once and the answer is at least 1.
3Intuition: every step outwards finds one new palindrome
Same as Problem 9: for each index i, treat it as the middle of an odd palindrome (left = right = i) and as the left half of an even middle (left = i, right = i + 1). Then expand outwards while the letters match.
The new idea: each time the while condition passes, the piece s[left..right] is a palindrome. It's a substring we haven't counted yet, so do count += 1 inside the loop, then step outwards. The teacher says the brute force's "check by shrinking" can just as well be "check by expanding", and expanding lets one call find several palindromes at once.
→ Neither. A palindrome s[a..b] has exactly one center: the letter at index (a+b)/2 if its length is odd, or the gap between indexes (a+b−1)/2 and (a+b+1)/2 if it's even. When we expand from that center, we pass through every size around it in order, so we reach s[a..b] exactly once. And no other center can reach it. All 2n − 1 centers are tried, so nothing is missed.
→ It can't be. Any bigger piece around that center contains the mismatched pair at mirror positions, so it isn't a palindrome either. Once a center fails, it's finished.
4Building the logic from the example
The while condition
Expanding moves left down and right up, so they can fall off the string: left can become −1 and right can become n. So the loop keeps going only while left >= 0, right < len(s), and s[left] == s[right], with the two boundary checks first so we never read a bad index.
left >= 0 check extra important in Python?→
s[-1] doesn't crash in Python. It quietly reads the last letter. Without the check, the loop compares the first letter with the last one and counts "palindromes" that wrap around the end. Example: "aab". After the even center finds "aa", L becomes −1 and R becomes 2, and s[−1] = "b" equals s[2] = "b", so it counts one fake palindrome. You'd get 5 instead of 4 (and "abab" would give 10 instead of 6).The teacher's walk on "aaa"
- i = 0, odd: L = R = 0, a letter matches itself → count 1 ("a" at 0). Next L = −1, outside → stop. Odd gave 1.
- i = 0, even: L = 0, R = 1, a = a → count 1 ("aa" at 0..1). Next L = −1 → stop. Even gave 1. So i = 0 adds 2.
- i = 1, odd: L = R = 1 → count ("a"). Step out: L = 0, R = 2, a = a → count ("aaa"). Step out: both outside → stop. Odd gave 2.
- i = 1, even: L = 1, R = 2, a = a → count ("aa" at 1..2). Next L = 0, R = 3, outside → stop. Even gave 1. So i = 1 adds 3.
- i = 2, odd: L = R = 2 → count ("a"). Next R = 3, outside → stop. Odd gave 1.
- i = 2, even: R = i + 1 = 3 is already outside → the loop never runs. Even gave 0. So i = 2 adds 1.
Total: 2 + 3 + 1 = 6 ✓, the same as the brute force.
→ The loop is identical. Only the return value changes. Problem 9 needed the length to rebuild the substring, so it returned
right − left − 1. Here we need how many palindromes the center produced, so we count each passing step and return that count. (A side fact: an odd center that reaches length L gives (L+1)/2 palindromes, and an even one gives L/2.)5Approach steps
count = 0.- For every index
i:count += expand(s, i, i)(odd) andcount += expand(s, i, i + 1)(even). expand(s, left, right): start a localfound = 0. While inside the string ands[left] == s[right]:found += 1, thenleft -= 1,right += 1. Returnfound.- Return
count.
6Code (Python)
class Solution:
def countSubstrings(self, s):
count = 0
for i in range(len(s)):
count += self.expand(s, i, i) # odd: center is the letter i
count += self.expand(s, i, i + 1) # even: center is the gap after i
return count
def expand(self, s, left, right):
found = 0
while left >= 0 and right < len(s) and s[left] == s[right]:
found += 1 # s[left..right] is a palindrome
left -= 1 # grow outwards
right += 1
return found7Code line by line
| line | what it means |
|---|---|
| for i in range(len(s)): | One loop. Each i gives one odd center and one even center (2n − 1 real centers in total). |
| count += self.expand(s, i, i) | Add every odd palindrome centred on letter i. This is always at least 1, the letter itself. |
| count += self.expand(s, i, i + 1) | Add every even palindrome centred on the gap after i. 0 if s[i] ≠ s[i+1] or i is the last index. |
| found = 0 | A fresh counter for this one center. |
| while left >= 0 and right < len(s) and s[left] == s[right]: | Boundaries first, then compare. If it passes, s[left..right] is a palindrome. |
| found += 1 | Count it right away. Each passing step is a different substring (one size bigger). |
| left -= 1 right += 1 | Try the next bigger piece around the same center. |
| return found | How many palindromes this center produced. |
8Dry run
s = "aaa". Each row is one call of expand.
| step | i | center | start L, R | pairs that pass (L,R) → substring | why it stops | found | count after |
|---|---|---|---|---|---|---|---|
| 1 | 0 | odd | 0, 0 | (0,0) "a" | L = −1 outside | 1 | 1 |
| 2 | 0 | even | 0, 1 | (0,1) "aa" | L = −1 outside | 1 | 2 |
| 3 | 1 | odd | 1, 1 | (1,1) "a", (0,2) "aaa" | L = −1, R = 3 outside | 2 | 4 |
| 4 | 1 | even | 1, 2 | (1,2) "aa" | R = 3 outside | 1 | 5 |
| 5 | 2 | odd | 2, 2 | (2,2) "a" | R = 3 outside | 1 | 6 |
| 6 | 2 | even | 2, 3 | none | R = 3 outside at once | 0 | 6 |
The string at the key moments (yellow = the pair being checked, grey = already counted inside):
How far each center grows, on "aaa" and on "abba" (an example with a mismatch):
"aaa" index: 0 1 2 odd @0 [a] → 1 even @0|1 [a a] → 1 odd @1 [a [a] a] → 2 "a", "aaa" even @1|2 [a a] → 1 odd @2 [a] → 1 even @2|? ✗ → 0 total 6 "abba" index: 0 1 2 3 odd @0 [a] → 1 even @0|1 ✗ (a≠b) → 0 odd @1 a [b] b → a≠b → 1 even @1|2 [a [b b] a] → 2 "bb", "abba" odd @2 a b [b] a → b≠a → 1 even @2|3 ✗ (b≠a) → 0 odd @3 [a] → 1 total 6
Final answers: "aaa" → 6 ✓, "abba" → 6 ✓ (a, b, b, a, bb, abba), "abc" → 3 ✓.
9Complexity & remember
- Time O(n²): one loop over n positions (2n − 1 centers), and each expansion grows at most about n/2 steps → n · n/2 → O(n²). We removed one whole loop compared to Part A.
- Space O(1): just a few counters and pointers.
- It was accepted and ran much faster than the brute force.
count += expand(i, i) + expand(i, i+1).In expand:
while left ≥ 0 and right < n and s[left] == s[right]: found += 1; left −= 1; right += 1.Every passing step = one new palindrome. Same loop as Problem 9, only the return value differs.
Part C · Revision page
| Brute force (Part A) | Expand around center (Part B) | |
|---|---|---|
| substrings come from | two loops (i, j) | one loop over 2n − 1 centers |
| pointers move | inwards (shrink) while left < right | outwards (expand) while inside and matching |
| when do we count | after the check returns True | every time the while condition passes |
| time / space | ≈ n³/2 (5·10⁸ for n = 1000) / O(1) | O(n²) / O(1) |
| Longest Palindromic Substring (Problem 9) | Palindromic Substrings (this one) | |
|---|---|---|
| asks for | the longest palindrome (a string) | how many palindromes (a number) |
| brute force: after a True check | keep it if j − i + 1 is longer | count += 1 |
| expand returns | length right − left − 1 | number of passing steps |
| main loop | identical: expand(i, i) and expand(i, i + 1) for every i | |
2. Every palindrome has exactly one center: a letter (odd) or a gap (even), 2n − 1 in all.
3. For each i, expand from (i, i) and from (i, i + 1).
4. Every step where the boundaries hold and the letters match = one more palindrome.
5. Stop at the first mismatch. Bigger pieces around that center can't be palindromes.
✗ comparing letters before the boundary checks (Python's
s[-1] won't crash, it gives a wrong count)✗ counting only once per center instead of once per passing step
✗ counting equal-letter substrings only once ("a" at index 0 and "a" at index 1 are two)
✗ building a new string for every (i, j) when indexes are enough
s = Solution()
print(s.countSubstrings("aaa")) # 6
print(s.countSubstrings("abc")) # 3
print(s.countSubstrings("a")) # 1
print(s.countSubstrings("aa")) # 3 (needs the even center)
print(s.countSubstrings("abba")) # 6
print(s.countSubstrings("aaaa")) # 10 (all the same letter: 4·5/2)Based on this video: Palindromic Substrings | Two Pointers