Longest palindromic Substring
Intuition
- Brute Force: Iterate every substring starting from each index , check if the substring is a palindrome. This will result in time complexity, as enumerating every substring will take , and checking a string is palindrome will take , where is the substring length. Overall, this method is quite slow due to we have to make a separate palindromic check for each substring.
Optimization can be achieved using dynamic programming.
Solution 1: DP
-
Optimization:
- Think about the most important attribute for a palindromic string:
- if a string is palindrome, after deleting its first and last letter, the remaining string will also be a palindrome. For example,
ababais a palindrome. After deleting the leadingaand tailinga,babis also a palindrome. Again, delete bothb, the remainingaitself is also a palindrome.
- if a string is palindrome, after deleting its first and last letter, the remaining string will also be a palindrome. For example,
- This could direct us to a dynamic programming solution. For a given starting index and ending index , a string is a palindrome, if and only if and is a palindrome. We can deduce the recurrence relation:
We can deduce the recurrence relation: . This shows a recursive dependency, where depends on , which in turn depends on , and so on, until a base case is reached.
- Think about the most important attribute for a palindromic string:
-
From the recurrence relation, it’s easy to know that at the end it will result in a single or two identical characters, which is the center of the palindrome.
- For odd length palindrome (e.g.
ababa), it will result in a single charactera. - For even length palindrome (e.g.
abba), it will result in two identical charactersbb.
- For odd length palindrome (e.g.
-
Boundaries:
- For string of length and , the substring would have an invalid length (0 or negative), so these are our base cases.
- A palindrome can have odd length or even length. The distinction is crucial for defining their ‘center’ from which they expand. For example, an odd-length palindrome like
ababahas a single character centera, while an even-length palindrome likeabbahas a two-character centerbb. The above recurrence relation naturally handles this by recursively shrinking the string towards its center. - In the DP approach, we explicitly handle base cases for substring length and length
-
DP iteration order:
-
Take a look at the recurrence equation, we can discover that the current state depends on , where is the next state, is the previous state. We can draw the recurrence relation matrix to find out that:
-
From the matrix we can see that, if we want to calculate the state , we need the state . This tells us we need to calculate first. So for the outer loop , we need to iterate in reversed order. For the inner loop , we need to iterate in ascending order.
-
class Solution {
public String longestPalindrome(String s) {
char[] ch = s.toCharArray();
int n = ch.length;
int maxLen = 1; //The current longest palindromic string length.
int left = 0, right = 0; //The starting index and ending index of the longest palindromic string.
boolean[][] f = new boolean[n][n];
//Initialization
f[n - 1][n - 1] = true;
//Starting from index n - 2, because we cannot iterate when starting index is n - 1.
for(int i = n - 2; i >= 0; i--){
f[i][i] = true; //Consider string of length 1 always a palindrome.
for(int j = i + 1; j < n; j++){
//Consider string of length 2
if(j - i == 1 && ch[i] == ch[j]){
f[i][j] = true;
}else{
//Consider string length > 2
f[i][j] = (f[i + 1][j - 1]) && (ch[i] == ch[j]);
}
if(f[i][j] && j - i + 1 > maxLen){
left = i;
right = j;
maxLen = j - i + 1;
}
}
}
return s.substring(left, right + 1);
}
}The above code can be faster by initializing cases when string length is 2 so that we don’t need to check the j - i == 1 for times but only times. This solution is faster than the previous solution.
Optimization 1: Less conditional check
class Solution {
public String longestPalindrome(String s) {
char[] ch = s.toCharArray();
int n = ch.length;
int maxLen = 1; //The current longest palindromic string length.
int left = 0, right = 0; //The starting index and ending index of the longest palindromic string.
boolean[][] f = new boolean[n][n];
//Initialization
f[n - 1][n - 1] = true;
//Consider string length of 2;
for(int i = 0; i < n - 1; i++){
if(ch[i] == ch[i + 1]){
f[i][i + 1] = true;
if(maxLen < 2){
maxLen = 2;
left = i;
right = i + 1;
}
}
}
//Starting from index n - 2, because we cannot iterate when starting index is n - 1.
for(int i = n - 2; i >= 0; i--){
f[i][i] = true; //Consider string of length 1 always a palindrome.
for(int j = i + 2; j < n; j++){
//Consider string length > 2
f[i][j] = (f[i + 1][j - 1]) && (ch[i] == ch[j]);
if(f[i][j] && j - i + 1 > maxLen){
left = i;
right = j;
maxLen = j - i + 1;
}
}
}
return s.substring(left, right + 1);
}
}Complexity
-
Time Complexity:
-
Space Complexity:
Optimization 2: Rolling array
From the recurrence equation, we can find that current state only depends on the next state. We can use a 1-dimensional array to optimize the previous solution. Here, we need to be cautious about the iteration order of the inner loop.
- When using 1-dimensional DP array, we override the
next unuseful state valuewithcurrent state value. For the outer loop, we still depend on the next state, so we should iterate in reverse order. For the inner loop, we depend on thepreviousstate, so we should iterate in ascending order. But when using rolling array, the previous state is always overridden by the current state. In order to use the correct state value, we need to iterate in reverse order for the inner loop when using 1-dimensional rolling array.

- From the illustration above, we can see what will happen if we iterate in ascending order when using a 1-dimensional array. We should update with , but in 1-dimensional array, we updated first, and it overrides the previous state, now when we want to update , we are calculating it based on the current state’s . That’s why we should iterate the inner loop in reverse order.
class Solution {
public String longestPalindrome(String s) {
char[] ch = s.toCharArray();
int n = ch.length;
int maxLen = 1; //The current longest palindromic string length.
int left = 0, right = 0; //The starting index and ending index of the longest palindromic string.
boolean[] f = new boolean[n];
//Initialization
f[n - 1] = true;
//Starting from index n - 2, because we cannot iterate when starting index is n - 1.
for(int i = n - 2; i >= 0; i--){
f[i] = true; //Consider string of length 1 always a palindrome.
for(int j = n - 1; j >= i + 1; j--){
//Consider string length >= 2
f[j] = j - i == 1 ? ch[i] == ch[j] : f[j - 1] && (ch[i] == ch[j]);
if(f[j] && j - i + 1 > maxLen){
left = i;
right = j;
maxLen = j - i + 1;
}
}
}
return s.substring(left, right + 1);
}
}Complexity
-
Time Complexity:
-
Space Complexity:
Solution 2: Double Pointers, expanding around center.
If we look into the recurrence equations in Solution 1:
The recurrence chain:
We can find that every can be derived from exactly one boundary condition, either , or , or .
This enlightens us that if we expand from each boundary conditions, we can eventually obtain all the states. The boundary conditions are the center of a palindromic string. By using this method, we can save the memory overhead of using a DP array.
- To apply both situation to the recurrence relation above, we need to discuss the odd and even length palindrome separately. The even length palindrome will exist if and only if . For each , we need to do another iteration if to find out the possible longest palindrome expanding from and .
Simple implementation is that for each index , we discuss the situations where the center length is or . If the center is palindromic, we expand around it, by moving double pointers and until . During this process, we update the maximum length and and its according starting and ending .
class Solution {
public String longestPalindrome(String s) {
char[] ch = s.toCharArray();
int n = ch.length;
int maxLen = 1; //The current longest palindromic string length.
int left = 0, right = 0; //starting and ending index of the longest palindromic string.
for(int i = 0; i < n; i++){
int j = i, k = i;
while(j >= 0 && k < n && ch[j] == ch[k]){
j--;
k++;
}
if(maxLen < k - j - 1){
maxLen = k - j - 1;
left = j + 1;
right = k - 1;
}
//Consider expanding from length 2.
j = i;
k = i + 1;
while(j >= 0 && k < n && ch[j] == ch[k]){
j--;
k++;
}
if(maxLen < k - j - 1){
maxLen = k - j - 1;
left = j + 1;
right = k - 1;
}
}
return s.substring(left, right + 1);
}
}-
Time complexity:
For each index, we expand around it, which could take at most steps. Given indices, the worst case is . But in reality, it is much faster than DP solution since it can stop during expanding.
-
Space complexity:
Optional optimization: Simplify code
We can unify the code by extracting the core logic into a method: expandAroundCenter , which returns the maximum palindrome length of the current expanding.
We don’t even need to save the left and right indices. By using the current center index and the length of maximum palindrome , we can calculate the and indices.
How to calculate the using and ?
Consider a string ababa ,when , = 5, should be , should be .
During each step, and move step. Assuming moved step, so does . The total , where represent the center character. So we have:
Also, it is clear that:
However, consider abba , we have to expand from and , resulting . Here, we have:
Simplify the equations:
So, in cases where is odd and even, we have:
How to unify the equations so that they can be applied to both situations when is odd and even?
When , , due to integer’s division will automatically floor the result.
Similarly, when , is even, .
So we have the unified equation:
Moreover, we don’t even need the to store the ending index. As long as we have and , we can directly have .
class Solution {
public String longestPalindrome(String s) {
char[] ch = s.toCharArray();
int n = ch.length;
int maxLen = 1; //The current longest palindromic string length.
int left = 0; //starting index of the current longest palindrome.
for(int i = 0; i < n; i++){
int len1 = expandAroundCenter(ch, i, i);
int len2 = expandAroundCenter(ch, i, i + 1);
int len = Math.max(len1, len2);
if(maxLen < len){
left = i - (len - 1) / 2;
maxLen = len;
}
}
return s.substring(left, left + maxLen);
}
private int expandAroundCenter(char[] s, int j, int k){
while(j >= 0 && k < s.length && s[j] == s[k]){
j--;
k++;
}
return k - j - 1;
}
}- Time complexity:
- Space Complexity: