Longest Palindromic Substring
1. Problem & Core Objective
Return the longest palindromic substring (contiguous) of s.
s = "babad" → "bab" (or "aba" — either is accepted)
s = "cbbd" → "bb"Constraints: 1 <= s.length <= 1000 · letters and digits
What's actually being tested: that expand-around-centre beats the DP table here — O(1) space instead of O(n²), same time. It's the question in this section where the tabulated DP is not the best answer, and knowing why matters more than knowing the DP.
2. First-Principles Thought Process
Every palindrome has a centre
A palindrome reads the same both ways, so it's symmetric about a centre. That centre is either:
- a character — odd length, like
"aba"centred onb - a gap between characters — even length, like
"abba"centred between the twobs
There are n character-centres and n − 1 gap-centres, so 2n − 1 centres in total.
Expand from each centre
From a centre, walk outward while the characters match. The moment they differ — or you run off either end — that centre's longest palindrome is complete.
Each expansion is O(n) and there are O(n) centres, so O(n²) time and O(1) space.
The DP alternative, and why it's worse here
isPal[i][j] is true when s[i..j] is a palindrome:
isPal[i][j] = (s[i] == s[j]) && (j - i < 3 || isPal[i+1][j-1])That's O(n²) time and O(n²) space — a million booleans at n = 1000.
Same time, worse space. The DP earns its place when the palindrome table is queried repeatedly — which is exactly Palindrome Partitioning, not this question.
The even/odd trap
Handling only odd-length centres finds "aba" but misses "bb". Both centre types must be tried at every position.
for (int i = 0; i < n; i++) {
expand(s, i, i); // odd — centred on a character
expand(s, i, i + 1); // even — centred on the gap after it
}Forgetting the even case fails on "cbbd", returning a single character.
3. Solution Paths
Approach 1 — Check every substring (brute force)
public String longestPalindrome(String s) {
String best = "";
for (int i = 0; i < s.length(); i++)
for (int j = i; j < s.length(); j++)
if (isPalindrome(s, i, j) && j - i + 1 > best.length())
best = s.substring(i, j + 1);
return best;
}
private boolean isPalindrome(String s, int lo, int hi) {
while (lo < hi) if (s.charAt(lo++) != s.charAt(hi--)) return false;
return true;
}- Time
O(n³)· SpaceO(1)
Counter-questions on this approach
⭐ "Where does the cubic come from?"
O(n²)substrings, each checked inO(n). Atn = 1000that's10^9character comparisons — too slow.And it's redundant: checking
s[i..j]re-examines the interior thats[i+1..j-1]already covered. Both the DP and expand-around-centre exploit that overlap; this version doesn't.
⭐ "Could you at least skip substrings shorter than the current best?"
Yes, and it helps in practice — iterate lengths descending and return the first palindrome found, or skip when
j - i + 1 <= best.length(). Both prune heavily on typical input.Neither changes the
O(n³)worst case, which is a string like"aaaa…a"where every substring is a palindrome and nothing is pruned.
"Is substring inside the loop a problem?"
It allocates
O(length)per call. Tracking(start, length)and materialising once at the end is strictly better — a constant-factor issue, but free to fix.
Approach 2 — DP table
public String longestPalindrome(String s) {
int n = s.length();
boolean[][] isPal = new boolean[n][n];
int start = 0, maxLen = 1;
for (int i = 0; i < n; i++) isPal[i][i] = true; // single characters
for (int len = 2; len <= n; len++) // by increasing length
for (int i = 0; i + len - 1 < n; i++) {
int j = i + len - 1;
if (s.charAt(i) != s.charAt(j)) continue;
if (len == 2 || isPal[i + 1][j - 1]) {
isPal[i][j] = true;
if (len > maxLen) { start = i; maxLen = len; }
}
}
return s.substring(start, start + maxLen);
}- Time
O(n²)· SpaceO(n²)
Counter-questions on this approach
⭐ "Why iterate by length rather than by index?"
Because
isPal[i][j]depends onisPal[i+1][j-1]— a shorter interval. Filling by increasing length guarantees the inner value is ready.Iterating
iandjin the natural nested order would read cells not yet computed. Getting the fill order right is the tabulation-specific obligation that memoisation handles automatically.
⭐ "Why len == 2 || in the condition?"
Because a length-2 palindrome has no interior to check —
isPal[i+1][j-1]would beisPal[i+1][i], an inverted range that's meaningless.With
"bb",i = 0, j = 1, so the interior would beisPal[1][0]. Thelen == 2short-circuit handles it before that read happens.Some implementations write
j - i < 3instead, which covers lengths 2 and 3 together — a length-3 palindrome also needs no interior check, since its middle character is trivially palindromic.
⭐ "It's O(n²) time like expand-around-centre. So why prefer the other?"
Space. This allocates
n²booleans — a million atn = 1000, which is 1 MB. Expand-around-centre isO(1).Same time, strictly worse space, and more code. The table is worth building only when you'll query it repeatedly — which is Palindrome Partitioning, not this problem.
That's the honest reason to know both: the DP isn't wrong here, it's just not the right tool.
Approach 3 — Expand around centre (optimal)
public String longestPalindrome(String s) {
int start = 0, maxLen = 1;
for (int i = 0; i < s.length(); i++) {
int odd = expand(s, i, i); // centred on a character
int even = expand(s, i, i + 1); // centred on the gap after it
int len = Math.max(odd, even);
if (len > maxLen) {
maxLen = len;
start = i - (len - 1) / 2; // recover the left edge
}
}
return s.substring(start, start + maxLen);
}
private int expand(String s, int lo, int hi) {
while (lo >= 0 && hi < s.length() && s.charAt(lo) == s.charAt(hi)) { lo--; hi++; }
return hi - lo - 1; // both overshot by one
}Trace — s = "babad", centre i = 2 (the second a):
| Step | lo | hi | s[lo] vs s[hi] | Action |
|---|---|---|---|---|
| start | 2 | 2 | a = a | expand |
| 1 | 1 | 3 | a vs a ✓ | expand |
| 2 | 0 | 4 | b vs d ✗ | stop |
| — | — | — | length 4 − 0 − 1 = 3 | "aba" |
- Time
O(n²)· SpaceO(1)
Counter-questions on this approach
⭐ "Why hi - lo - 1 for the length?"
Because the loop exits after both pointers have moved one step past the palindrome —
lois one left of the start,hione right of the end.So the palindrome spans
lo+1 .. hi-1, whose length is(hi-1) - (lo+1) + 1=hi - lo - 1.It's worth deriving rather than memorising: on
"aba"the loop ends withlo = -1, hi = 3, giving3 - (-1) - 1 = 3✓
⭐ "Explain start = i - (len - 1) / 2."
iis the centre index andlenis the palindrome's length; the left edge is(len-1)/2characters before the centre.Integer division makes this work for both parities. Odd length 3 centred at
i:(3-1)/2 = 1, sostart = i - 1✓. Even length 4 with centresiandi+1:(4-1)/2 = 1, sostart = i - 1✓ — because the even expansion started at(i, i+1), and the left half extends one further fromi.That the same formula covers both cases is a genuine convenience, not an accident — and it's the line most likely to be wrong, so I'd check it on a length-2 example:
"bb"ati = 0giveslen = 2,start = 0 - 0 = 0✓.
⭐ "Why must both expand(i, i) and expand(i, i+1) be called?"
Because palindromes come in both parities and they have different centre types. Odd-length ones are centred on a character; even-length ones on a gap.
Calling only the odd version finds
"aba"but misses"bb"entirely — on"cbbd"it would return a single character instead of"bb".There are
ncharacter-centres andn−1gap-centres, hence2n − 1total.
"Does expand(s, i, i+1) overflow at the last index?"
No — the loop guard checks
hi < s.length()before indexing, so wheni = n−1the condition fails immediately and it returnshi - lo - 1=n - (n-1) - 1= 0. A zero-length result that never wins the max.
"What's the worst case?"
A string of identical characters, like
"aaaa…a". Every centre expands the full width, so it's genuinelyO(n²)—10^6atn = 1000, which is fine.
"Is there anything faster?"
Manacher's algorithm is
O(n). It reuses information from previously computed centres, exploiting the symmetry of the palindrome already found. It's genuinely linear but substantially harder to write correctly, and atn = 1000theO(n²)version runs in about a millisecond. Worth naming, not worth writing.
Comparison
| Approach | Time | Space | Notes |
|---|---|---|---|
| All substrings | O(n³) | O(1) | 10^9 at the limit |
| DP table | O(n²) | O(n²) | Right tool when the table is queried |
| Expand around centre | O(n²) | O(1) | The answer |
| Manacher's | O(n) | O(n) | Optimal; hard to write |
4. Why the Optimal Wins
Against the cubic version: both the DP and expansion exploit that a palindrome's interior is itself a palindrome, so the O(n) re-check disappears.
Against the DP: identical time, but O(1) space instead of O(n²). The table stores the palindromic status of every interval when the question asks for just one — the longest.
The DP isn't wrong, it's over-built for this question. It becomes the right tool in Palindrome Partitioning, where isPal[i][j] is queried repeatedly during a search.
The framing worth keeping:
Every palindrome has a centre — a character or a gap — so there are
2n − 1centres. Expand from each while the characters match.O(n²)time,O(1)space, and both parities must be tried.
5. Java Prerequisites
Expand-around-centre
private int expand(String s, int lo, int hi) {
while (lo >= 0 && hi < s.length() && s.charAt(lo) == s.charAt(hi)) { lo--; hi++; }
return hi - lo - 1; // both overshot by one
}Both parities, every position
expand(s, i, i); // odd
expand(s, i, i + 1); // evenRecovering the start — start = i - (len - 1) / 2, correct for both parities by integer division.
Track indices, not strings — build the substring once at the end rather than allocating inside the loop.
6. Interview Communication Guide
Clarifying questions: Substring (contiguous) or subsequence (substring)? If several are tied for longest, any of them (yes)? Is a single character a palindrome (yes)? Case-sensitive (assume yes)?
The pitch
"Every palindrome is symmetric about a centre, and that centre is either a character for odd lengths or a gap between characters for even ones. So there are
2n − 1centres.From each, I expand outward while the characters match, stopping at the first mismatch or either edge. Each expansion is
O(n)and there areO(n)centres, soO(n²)time andO(1)space.Both parities must be tried at every position. Handling only character-centres finds
'aba'but misses'bb'— on'cbbd'it would return a single character.I'd mention the DP alternative because it's the expected answer for this section:
isPal[i][j] = s[i] == s[j] && isPal[i+1][j-1], filled by increasing length so the shorter interval is ready. That's alsoO(n²)time butO(n²)space — a million booleans atn = 1000.Same time, worse space, more code. The table is worth building when you'll query it repeatedly, which is Palindrome Partitioning — not here, where you want one answer.
Two details in the expansion. The length is
hi - lo - 1because both pointers overshoot by one when the loop exits. And the start isi - (len-1)/2, which works for both parities thanks to integer division — worth checking on a length-2 case rather than trusting.There's an
O(n)solution, Manacher's, which reuses symmetry from previously computed centres. It's genuinely linear but hard to write correctly, and atn = 1000the quadratic version runs in about a millisecond."
Edge cases to volunteer:
| Input | Expected | Tests |
|---|---|---|
"a" | "a" | Single character |
"ab" | "a" or "b" | No multi-char palindrome |
"cbbd" | "bb" | Even-length — the parity trap |
"babad" | "bab" or "aba" | Ties accepted |
"aaaa" | "aaaa" | Worst case — every centre expands fully |
"abacdfgdcaba" | "aba" | Longest isn't at the centre of the string |
Name "cbbd". It's the minimal case that fails without the even-centre expansion, and a solution missing it returns a single character while passing "babad".
7. Follow-Up Questions — Modified Constraints
⭐ "Count all palindromic substrings instead of finding the longest."
Question 6. Same expansion, but count each successful step rather than tracking a maximum — every widening of a centre is one more palindrome.
O(n²)time,O(1)space, and the change is a single line.
⭐ "Find the longest palindromic SUBSEQUENCE."
A completely different problem — LeetCode 516. Subsequences needn't be contiguous, so there's no centre to expand from. It's a 2-D DP:
dp[i][j] = dp[i+1][j-1] + 2when the ends match, elsemax(dp[i+1][j], dp[i][j-1]).O(n²)time and space. Worth naming that "substring" and "subsequence" change the algorithm entirely.
"Return all longest palindromic substrings when there are ties."
Collect every centre achieving the maximum length in a second pass, or track a list during the first.
O(n²)still; the output could beO(n)strings.
"What if n were 10^5?"
O(n²)is10^10— too slow. Manacher'sO(n)becomes necessary. That's the threshold where the harder algorithm earns its complexity.
"Find the longest palindrome you can make by deleting at most k characters."
Much harder — it's the longest palindromic subsequence with a budget, an
O(n² k)DP. The centre-expansion idea doesn't survive deletions.
"Check whether the whole string can be rearranged into a palindrome."
Not a substring problem at all — count character frequencies and check that at most one is odd.
O(n). Worth mentioning because it sounds related and shares nothing.