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 · Before starting

Words used on this page

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.

shapehow the pointers moveexample
Opposite endsleft 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 directionslow "write" and fast "read" pointers both go left to right.Move Zeroes
Three pointerslow / mid / high split the array into the 0s, 1s, unknown part and 2s.Sort Colors
Fix one + two pointersa loop fixes one value, and two pointers search the rest, skipping duplicates.3Sum
left-max / right-maxopposite ends, each remembering the tallest bar seen. The smaller side moves.Trapping Rain Water
Both ends of a stringcompare s[left] and s[right], then move both inwards.Valid Palindrome, Part A here
Expand around a centerstart 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.

check 1abbaa = a ✓ → move both in
L  R
check 2abbab = b ✓ → move in → L > R, crossed → palindrome
 LR 

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.

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":

substringindexespalindrome?
a0..0yes
a1..1yes
a2..2yes
aa0..1yes
aa1..2yes
aaa0..2yes

All 6 substrings are palindromes → answer 6. Another example (LeetCode's first): "abc" → only the three single letters → answer 3.

"aaa"aaa"aa" at 0..1
"aaa"aaa"aa" at 1..2, a different substring, counted again

2What the constraints tell us

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:

  1. Make every substring with a start i and an end j (j goes from i to the end).
  2. Check each one with the shrinking two-pointer check.
  3. 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:

  1. Build it: keep a string sub for each i. Every time j moves, add s[j] to the end. Then sub is exactly s[i..j]: "a", then "aa", then "aaa". Pass sub to the check.
  2. Don't build it, pass the indexes: give the check the whole string plus i and j as 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.
Doubt: which of the two is better?
→ 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.
Doubt: "two pointers only works on sorted arrays", so why can we use it on an unsorted string?
→ 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

  1. count = 0.
  2. For every start i, for every end j from i to n−1:
  3. If isPalindrome(s, i, j) (shrinking two pointers), do count += 1.
  4. Return count.

6Code (Python)

Brute force, passing indexes · O(n³)
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 True
Brute force, building the substring (the other way she shows)
class 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 True

7Code line by line

linewhat it means
count = 0How 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 += 1No "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 FalseOne mismatched pair is enough to say no.
left += 1; right -= 1The 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".

ijs[i..j]pairs compared (left, right)palindrome?count after
00anone (left = right)yes1
01aa(0,1) a=a → crossyes2
02aaa(0,2) a=a → meet at 1yes3
11anoneyes4
12aa(1,2) a=a → crossyes5
22anoneyes6

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.

i=0, j=2aaaa = a ✓ → move in → L = R = 1 → stop → palindrome, count 3
L R

Final answer for "aaa": 6 ✓

9Complexity & remember

Remember the brute force 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

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.

Doubt: could the same palindrome be counted twice, or one be missed?
→ 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.
Doubt: why stop at the first mismatch? A bigger piece around the same center might still be a palindrome?
→ 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.

Doubt (Python): why is the 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"

Total: 2 + 3 + 1 = 6 ✓, the same as the brute force.

Doubt: how is this different from the expand in Problem 9?
→ 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

  1. count = 0.
  2. For every index i: count += expand(s, i, i) (odd) and count += expand(s, i, i + 1) (even).
  3. expand(s, left, right): start a local found = 0. While inside the string and s[left] == s[right]: found += 1, then left -= 1, right += 1. Return found.
  4. Return count.

6Code (Python)

Expand around center · O(n²) time, O(1) space
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 found

7Code line by line

linewhat 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 = 0A 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 += 1Count it right away. Each passing step is a different substring (one size bigger).
left -= 1 right += 1Try the next bigger piece around the same center.
return foundHow many palindromes this center produced.

8Dry run

s = "aaa". Each row is one call of expand.

stepicenterstart L, Rpairs that pass (L,R) → substringwhy it stopsfoundcount after
10odd0, 0(0,0) "a"L = −1 outside11
20even0, 1(0,1) "aa"L = −1 outside12
31odd1, 1(1,1) "a", (0,2) "aaa"L = −1, R = 3 outside24
41even1, 2(1,2) "aa"R = 3 outside15
52odd2, 2(2,2) "a"R = 3 outside16
62even2, 3noneR = 3 outside at once06

The string at the key moments (yellow = the pair being checked, grey = already counted inside):

step 3aaaaodd center i = 1 → "a" ✓ found 1
 LR 
step 3baaastep out: a = a ✓ → "aaa" found 2, next step falls off both ends
L R
step 4aaaeven center between 1 and 2 → "aa" ✓ found 1
 LR

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

Remember the counting version For every i: 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 fromtwo loops (i, j)one loop over 2n − 1 centers
pointers moveinwards (shrink) while left < rightoutwards (expand) while inside and matching
when do we countafter the check returns Trueevery 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 forthe longest palindrome (a string)how many palindromes (a number)
brute force: after a True checkkeep it if j − i + 1 is longercount += 1
expand returnslength right − left − 1number of passing steps
main loopidentical: expand(i, i) and expand(i, i + 1) for every i
If you remember only 5 lines 1. Substrings at different positions count separately ("aaa" → 6).
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.
Mistakes to avoid ✗ forgetting the even centers ("aa" would give 2 instead of 3)
✗ 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
test it yourself (paste under any Solution above)
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