5. Longest Palindromic Substring
Problem Description
Given a string s
, you need to find and return the longest palindromic substring within it.
A palindrome is a string that reads the same forwards and backwards. For example, "racecar" is a palindrome, as is "noon". A substring is a contiguous sequence of characters within the string.
The problem asks you to identify the longest substring of s
that forms a palindrome. If there are multiple palindromic substrings with the same maximum length, you can return any one of them.
For example:
- If
s = "babad"
, the answer could be either"bab"
or"aba"
since both are palindromic substrings of length 3 - If
s = "cbbd"
, the answer would be"bb"
- If
s = "a"
, the answer would be"a"
itself
The solution uses dynamic programming with a 2D table f[i][j]
where each entry indicates whether the substring from index i
to index j
is a palindrome. The algorithm works by:
- Initializing all single characters as palindromes (
f[i][i] = True
) - Checking longer substrings by comparing characters at positions
i
andj
- If
s[i] == s[j]
, then the substrings[i..j]
is a palindrome if and only ifs[i+1..j-1]
is also a palindrome - Tracking the starting position (
k
) and length (mx
) of the longest palindrome found - Returning the substring from position
k
with lengthmx
The time complexity is O(n²)
where n
is the length of the string, as we need to check all possible substrings. The space complexity is also O(n²)
for the DP table.
Intuition
The key insight for solving this problem is recognizing that palindromes have a recursive structure. A string is a palindrome if its first and last characters match AND the substring between them is also a palindrome.
Think about checking if "racecar" is a palindrome. We see that r
equals r
at both ends. But that's not enough - we also need "aceca" to be a palindrome. Similarly, "aceca" is a palindrome if a
equals a
and "cec" is a palindrome, and so on.
This recursive relationship suggests we can build up our answer from smaller substrings to larger ones. If we already know whether all smaller substrings are palindromes, we can quickly determine if a larger substring is a palindrome by just checking its outer characters.
This leads us to dynamic programming. We can create a table where f[i][j]
tells us whether the substring from index i
to j
is a palindrome. The beauty is that we can fill this table systematically:
- Every single character is a palindrome by itself (base case)
- For any substring
s[i..j]
, ifs[i] != s[j]
, it's definitely not a palindrome - If
s[i] == s[j]
, then we just need to check ifs[i+1..j-1]
is a palindrome
The tricky part is the order of computation. Since f[i][j]
depends on f[i+1][j-1]
, we need to compute smaller substrings before larger ones. This is why the code iterates i
backwards (from n-2
to 0
) and j
forwards (from i+1
to n-1
) - this ensures that when we compute f[i][j]
, the value f[i+1][j-1]
has already been computed.
As we build the table, we keep track of the longest palindrome found so far by updating the starting position and length whenever we find a longer one.
Learn more about Two Pointers and Dynamic Programming patterns.
Solution Approach
The solution uses dynamic programming with a 2D boolean table to track palindromic substrings.
Data Structure Setup:
- Create a 2D table
f[i][j]
wheref[i][j] = True
means substrings[i..j]
is a palindrome - Initialize all entries to
True
initially (this handles the base case wherei == j
, single characters) - Track the longest palindrome with variables
k
(starting index) andmx
(length), initialized ask = 0, mx = 1
Core Algorithm: The implementation follows a specific iteration pattern to ensure dependencies are resolved correctly:
-
Outer loop: Iterate
i
fromn-2
down to0
(backwards)- We start from
n-2
because wheni = n-1
, there are no validj > i
positions to check
- We start from
-
Inner loop: For each
i
, iteratej
fromi+1
ton-1
(forwards)- This ensures we're only checking substrings of length 2 or more
-
Palindrome Check Logic:
f[i][j] = False # Initialize as not palindrome if s[i] == s[j]: f[i][j] = f[i+1][j-1]
- First assume substring is not a palindrome
- If outer characters match (
s[i] == s[j]
), check the inner substring - The value
f[i+1][j-1]
tells us if the inner substring is a palindrome - Due to our iteration order,
f[i+1][j-1]
is already computed when we need it
-
Track Maximum:
- Whenever we find a palindrome (
f[i][j] = True
) - Check if its length
j - i + 1
exceeds current maximummx
- If yes, update
k = i
andmx = j - i + 1
- Whenever we find a palindrome (
Why This Iteration Order Works:
- When computing
f[i][j]
, we needf[i+1][j-1]
- Since
i+1 > i
andj-1 < j
, we need to compute rows from bottom to top (i
decreasing) - Within each row, we compute columns from left to right (
j
increasing) - This guarantees all dependencies are satisfied
Return Result:
Finally, return the substring s[k : k + mx]
which represents the longest palindrome found, starting at index k
with length mx
.
The time complexity is O(n²)
as we check all possible substring pairs, and space complexity is O(n²)
for the DP table.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the algorithm with s = "babad"
.
Initial Setup:
- String:
s = "babad"
(indices 0-4) - Create a 5×5 table
f[i][j]
initialized toTrue
- Variables:
k = 0
,mx = 1
(initially the first character "b")
Iteration Process:
When i = 3 (starting from n-2 = 3):
- j = 4: Check substring "a[3]d[4]"
s[3] ≠ s[4]
('a' ≠ 'd')f[3][4] = False
When i = 2:
- j = 3: Check substring "b[2]a[3]"
s[2] ≠ s[3]
('b' ≠ 'a')f[2][3] = False
- j = 4: Check substring "b[2]ad[4]"
s[2] ≠ s[4]
('b' ≠ 'd')f[2][4] = False
When i = 1:
- j = 2: Check substring "a[1]b[2]"
s[1] ≠ s[2]
('a' ≠ 'b')f[1][2] = False
- j = 3: Check substring "a[1]ba[3]"
s[1] = s[3]
('a' = 'a')f[1][3] = f[2][2] = True
(single character 'b' is palindrome)- Found palindrome "aba" of length 3 > mx(1)
- Update:
k = 1
,mx = 3
- j = 4: Check substring "a[1]bad[4]"
s[1] ≠ s[4]
('a' ≠ 'd')f[1][4] = False
When i = 0:
- j = 1: Check substring "b[0]a[1]"
s[0] ≠ s[1]
('b' ≠ 'a')f[0][1] = False
- j = 2: Check substring "b[0]ab[2]"
s[0] = s[2]
('b' = 'b')f[0][2] = f[1][1] = True
(single character 'a' is palindrome)- Found palindrome "bab" of length 3 = mx(3)
- No update needed (same length)
- j = 3: Check substring "b[0]aba[3]"
s[0] ≠ s[3]
('b' ≠ 'a')f[0][3] = False
- j = 4: Check substring "b[0]abad[4]"
s[0] ≠ s[4]
('b' ≠ 'd')f[0][4] = False
Final DP Table (True/False):
0 1 2 3 4 0 [ T F T F F ] 1 [ T T F T F ] 2 [ T T T F F ] 3 [ T T T T F ] 4 [ T T T T T ]
Result:
- Return
s[k : k + mx] = s[1:4] = "aba"
- The algorithm found "aba" as the longest palindrome (length 3)
Note: The algorithm could also have returned "bab" if we had updated k
when finding palindromes of equal length. Both are valid answers.
Solution Implementation
1class Solution:
2 def longestPalindrome(self, s: str) -> str:
3 """
4 Find the longest palindromic substring using dynamic programming.
5
6 Args:
7 s: Input string
8
9 Returns:
10 The longest palindromic substring
11 """
12 n = len(s)
13
14 # dp[i][j] represents whether substring s[i:j+1] is a palindrome
15 # Initialize all single characters as palindromes (True)
16 dp = [[True] * n for _ in range(n)]
17
18 # Variables to track the starting position and length of longest palindrome
19 start_pos = 0
20 max_length = 1
21
22 # Fill the dp table bottom-up (from longer indices to shorter)
23 for i in range(n - 2, -1, -1):
24 for j in range(i + 1, n):
25 # Initially mark as False for substrings with length > 1
26 dp[i][j] = False
27
28 # Check if characters at both ends match
29 if s[i] == s[j]:
30 # For substrings of length 2: just check if ends match
31 # For longer substrings: also check if inner substring is palindrome
32 dp[i][j] = dp[i + 1][j - 1]
33
34 # Update longest palindrome if current one is longer
35 if dp[i][j] and max_length < j - i + 1:
36 start_pos = i
37 max_length = j - i + 1
38
39 # Return the longest palindromic substring
40 return s[start_pos : start_pos + max_length]
41
1class Solution {
2 public String longestPalindrome(String s) {
3 int n = s.length();
4
5 // dp[i][j] represents whether substring s[i...j] is a palindrome
6 boolean[][] dp = new boolean[n][n];
7
8 // Initialize all single characters as palindromes (diagonal elements)
9 for (boolean[] row : dp) {
10 Arrays.fill(row, true);
11 }
12
13 // Variables to track the starting index and length of longest palindrome
14 int startIndex = 0;
15 int maxLength = 1;
16
17 // Build the DP table bottom-up (from end to start of string)
18 for (int i = n - 2; i >= 0; i--) {
19 for (int j = i + 1; j < n; j++) {
20 // Reset to false for substrings of length > 1
21 dp[i][j] = false;
22
23 // Check if current characters match
24 if (s.charAt(i) == s.charAt(j)) {
25 // If characters match, check if inner substring is palindrome
26 // For substrings of length 2, dp[i+1][j-1] is true (single char or empty)
27 dp[i][j] = dp[i + 1][j - 1];
28
29 // Update longest palindrome if current is longer
30 if (dp[i][j] && maxLength < j - i + 1) {
31 maxLength = j - i + 1;
32 startIndex = i;
33 }
34 }
35 }
36 }
37
38 // Return the longest palindromic substring
39 return s.substring(startIndex, startIndex + maxLength);
40 }
41}
42
1class Solution {
2public:
3 string longestPalindrome(string s) {
4 int n = s.size();
5
6 // dp[i][j] represents whether substring s[i...j] is a palindrome
7 // Initialize all to true (single characters and empty strings are palindromes)
8 vector<vector<bool>> dp(n, vector<bool>(n, true));
9
10 // Variables to track the longest palindrome found
11 int startIndex = 0; // Starting index of longest palindrome
12 int maxLength = 1; // Length of longest palindrome (at least 1)
13
14 // Build the DP table bottom-up
15 // Start from the second-to-last row and move upward
16 for (int i = n - 2; i >= 0; --i) {
17 // For each starting position i, check all ending positions j > i
18 for (int j = i + 1; j < n; ++j) {
19 // Initially assume substring s[i...j] is not a palindrome
20 dp[i][j] = false;
21
22 // Check if first and last characters match
23 if (s[i] == s[j]) {
24 // If they match, the substring is a palindrome if:
25 // - The inner substring s[i+1...j-1] is also a palindrome
26 // - For substrings of length 2, this is automatically true
27 dp[i][j] = dp[i + 1][j - 1];
28
29 // Update longest palindrome if current one is longer
30 if (dp[i][j] && maxLength < j - i + 1) {
31 maxLength = j - i + 1;
32 startIndex = i;
33 }
34 }
35 }
36 }
37
38 // Return the longest palindromic substring
39 return s.substr(startIndex, maxLength);
40 }
41};
42
1/**
2 * Finds the longest palindromic substring in the given string
3 * using dynamic programming approach
4 * @param s - Input string
5 * @returns The longest palindromic substring
6 */
7function longestPalindrome(s: string): string {
8 const length: number = s.length;
9
10 // Create a 2D DP table where dp[i][j] indicates whether
11 // substring from index i to j is a palindrome
12 const isPalindrome: boolean[][] = Array(length)
13 .fill(0)
14 .map(() => Array(length).fill(true));
15
16 // Track the starting index of the longest palindrome found
17 let startIndex: number = 0;
18 // Track the maximum length of palindrome found
19 let maxLength: number = 1;
20
21 // Fill the DP table bottom-up
22 // Start from the second last row and move upward
23 for (let i = length - 2; i >= 0; i--) {
24 // For each row, check all columns to the right of diagonal
25 for (let j = i + 1; j < length; j++) {
26 // Initially mark as false
27 isPalindrome[i][j] = false;
28
29 // Check if characters at current positions match
30 if (s[i] === s[j]) {
31 // If matching, palindrome status depends on inner substring
32 // For substrings of length 2, they are palindromes if characters match
33 // For longer substrings, check if inner substring is also palindrome
34 isPalindrome[i][j] = isPalindrome[i + 1][j - 1];
35
36 // Update the longest palindrome if current one is longer
37 if (isPalindrome[i][j] && maxLength < j - i + 1) {
38 maxLength = j - i + 1;
39 startIndex = i;
40 }
41 }
42 }
43 }
44
45 // Extract and return the longest palindrome substring
46 return s.slice(startIndex, startIndex + maxLength);
47}
48
Time and Space Complexity
The time complexity is O(n^2)
, where n
is the length of the string s
. This is because the code uses two nested loops - the outer loop iterates from n-2
to 0
(approximately n
iterations), and the inner loop iterates from i+1
to n-1
for each i
. The total number of iterations across both loops is approximately n*(n-1)/2
, which simplifies to O(n^2)
.
The space complexity is O(n^2)
, where n
is the length of the string s
. This is due to the 2D boolean array f
of size n x n
that stores whether each substring s[i:j+1]
is a palindrome. The array requires n^2
space to store all possible substring palindrome states. The additional variables k
and mx
only require O(1)
space, so the overall space complexity remains O(n^2)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Initialization of the DP Table
The Pitfall:
Many implementations mistakenly initialize the entire DP table as False
, forgetting that single characters are always palindromes. This leads to incorrect results when checking substrings of length 2.
Why It Happens:
When checking if s[i..j]
is a palindrome where j = i + 1
(substring of length 2), the code checks dp[i+1][j-1]
which becomes dp[i+1][i]
. If this wasn't properly initialized as True
, the algorithm incorrectly concludes that two matching characters don't form a palindrome.
Example of Incorrect Code:
# WRONG: This initialization causes issues
dp = [[False] * n for _ in range(n)] # All False including diagonals
The Solution:
Initialize the diagonal elements (where i == j
) as True
since single characters are palindromes:
# CORRECT: Initialize with True to handle base cases
dp = [[True] * n for _ in range(n)]
Or explicitly set diagonal values:
dp = [[False] * n for _ in range(n)]
for i in range(n):
dp[i][i] = True # Single characters are palindromes
2. Wrong Iteration Order Leading to Uncomputed Dependencies
The Pitfall:
Iterating in the wrong order causes the algorithm to access dp[i+1][j-1]
before it's been computed, leading to incorrect results.
Example of Incorrect Iteration:
# WRONG: Top-down iteration doesn't guarantee dependencies are ready
for i in range(n): # Going forward instead of backward
for j in range(i + 1, n):
if s[i] == s[j]:
dp[i][j] = dp[i+1][j-1] # dp[i+1][j-1] might not be computed yet!
The Solution:
Always iterate i
from n-2
down to 0
(backwards) to ensure dp[i+1][j-1]
is computed before dp[i][j]
:
# CORRECT: Bottom-up approach ensures dependencies are satisfied
for i in range(n - 2, -1, -1): # Backward iteration
for j in range(i + 1, n): # Forward iteration
# Now dp[i+1][j-1] is guaranteed to be computed
3. Not Handling Edge Cases for Short Substrings
The Pitfall:
When j = i + 1
(checking a 2-character substring), accessing dp[i+1][j-1]
becomes dp[i+1][i]
, which crosses the diagonal. Without proper initialization, this can cause index errors or incorrect results.
The Solution:
The initialization of dp
as all True
elegantly handles this case. When checking a 2-character substring where both characters match, dp[i+1][i]
correctly returns True
, confirming the palindrome.
Alternatively, you can add explicit length checks:
if s[i] == s[j]: if j - i == 1: # Two characters dp[i][j] = True else: # More than two characters dp[i][j] = dp[i+1][j-1]
Which of the following shows the order of node visit in a Breadth-first Search?
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!