最长回文子串与最长回文子序列
最长回文子串
LeetCode 链接: 5. 最长回文子串
题目描述
给定一个字符串 s
,找到 s
中最长的回文子串。你可以假设 s
的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
dp解法
如果我们已经知道 “bab”
是回文子串,那么很明显 “ababa”
一定是回文子串,因为这两个左右端字母是相同的。假设 S
是给定的字符串,
S
i
S_i
Si 表示 S
在位置 i
的字符
定义状态:
d p ( i , j ) = { t r u e , 当 S i . . . S j 是回文子串 f a l s e , 其它情况 dp(i,j)=\begin{cases} true, & \text {当} S_i...S_j \text{是回文子串} \\ false, & \text {其它情况} \end{cases} dp(i,j)={true,false,当Si...Sj是回文子串其它情况
初始状态:
d p ( i , i ) = t r u e dp(i,i)=true dp(i,i)=true
d p ( i , i + 1 ) = ( S i = = S j ) dp(i,i+1)=(S_i==S_j) dp(i,i+1)=(Si==Sj)
转移方程:
d p ( i , j ) = { S i = = S j & & d p ( i + 1 , j − 1 ) , j − i > 2 S i = = S j , 1 < = j − i < = 2 t r u e j − i = 0 dp(i,j) = \begin{cases} S_i==S_j \ \&\& \ dp(i+1,j-1), & j-i>2 \\ S_i==S_j, & 1<=j-i<=2 \\ true & j-i=0 \end{cases} dp(i,j)=⎩⎪⎨⎪⎧Si==Sj && dp(i+1,j−1),Si==Sj,truej−i>21<=j−i<=2j−i=0
需要注意的是,求 dp(i+1,j-1)
时要保证 j-i > 2
,防止出现越界错误
public String longestPalindrome(String s) {
if (s == null || s.length() == 0)
return s;
int n = s.length();
char[] str = s.toCharArray();
String res = null;
boolean[][] dp = new boolean[n][n];
for (int i = n - 1; i >= 0; i--) {
for (int j = i; j < n; j++) {
dp[i][j] = str[i] == str[j] && (j - i <= 2 || dp[i + 1][j - 1]);
if (dp[i][j] && (res == null || j - i + 1 > res.length())) {
res = s.substring(i, j + 1);
}
}
}
return res;
}
时间复杂度: O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( n 2 ) O(n^2) O(n2)
n
是字符串长度
从中心扩展
考虑到回文子串是关于中心对称的,因此一个回文子串可以从中心向两边扩展。实际就是暴力搜索的思想,从回文子串的中心向两边展开搜索匹配。另外回文子串的长度可能是奇数也可能是偶数,从而,不仅要以每个字符为中心搜索(此时回文子串长度为奇数),而且还要以相邻字符之间的位置为中心搜索(此时回文子串长度为偶数),因此我们需要搜索
2
n
−
1
2n-1
2n−1 个中心。 例如,对于字符串 aba
,中心是字符 b
,对于字符串 abba
,中心是两个 b
之间的位置。
public String longestPalindrome(String s) {
if (s == null || s.length() == 0)
return s;
// 记录当前最长回文子串开始字符和结束字符的位置
int start = 0, end = 0;
for (int i = 0; i < s.length(); ++i) {
// 回文中心是字符
int len1 = searchFromCenter(s, i, i);
// 回文中心在字符之间
int len2 = searchFromCenter(s, i, i + 1);
int len = Math.max(len1, len2);
// 出现更长的回文子串,更新位置信息
if (len > end - start) {
// 统一处理成开始字符和结束字符的位置
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substring(start, end + 1);
}
private int searchFromCenter(String s, int left, int right) {
int l = left, r = right;
while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {
l--;
r++;
}
return r - l - 1;
}
时间复杂度: O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( 1 ) O(1) O(1)
n
是字符串长度
马拉车算法
关于马拉车算法,单独记录了一下自己的理解:
理解 Manacher’s Algorithm(马拉车算法)——最长回文子串问题
public String longestPalindrome(String s) {
/**
字符串处理,例如 S = "babad",处理后 T = "@#b#a#b#a#d#$"
要在每个字符之间以及字符串首部和尾部添加'#',T 的长度变为2*s.length()+1,另外又要添加'@'和'$'防止越界,因此最终 T 的长度为2*s.length()+3
**/
char[] T = new char[2*s.length()+3];
T[0]='@'; // 避免越左边界,因为'@'肯定和右边的字符不一样,匹配到'@'循环终止
T[1]='#';
T[T.length-1]='$'; // 避免越右边界,因为'$'肯定和左边的字符不一样,匹配到'$'循环终止
int t = 2; // 在首部添加特殊字符后,从下标 2 开始
for(char c : s.toCharArray()){
T[t++]=c;
T[t++]='#';
}
// maxLen 记录 T 中最长回文子串的长度,maxCenter为最长回文子串的中心
int maxLen=0, maxCenter=0;
// center为上一个最长回文串的中心,right为其最右端字符的位置
int[] R = new int[T.length];
int center = 0, right = 0;
// Manacher's algorithm
for (int i = 1; i < R.length - 1; ++i) {
//算法核心部分
if (i < right)
R[i] = Math.min(right - i, R[2 * center - i]);
// 从T[i]依次向两边匹配
while (T[i + R[i] + 1] == T[i - R[i] - 1])
R[i]++;
// 更新回文串中心和最右端位置
if (i + R[i] > right) {
center = i;
right = i + R[i];
}
// 如果出现更长的回文子串,更新 maxLen 和 maxCenter
if(maxLen < R[i]){
maxLen = R[i];
maxCenter = i;
}
}
// 计算最长回文子串在S中的起始位置
int index = (maxCenter - 1 - maxLen) / 2;
//返回S中的最长回文子串
return s.substring(index, index + maxLen);
}
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)
n
是字符串长度
其他解法:字符串哈希。掌握后再补充
最长回文子序列
LeetCode 链接: 516. 最长回文子序列
题目描述
给定一个字符串s
,找到其中最长的回文子序列。可以假设s
的最大长度为1000
。
示例 1:
输入:
"bbbab"
输出:
4
一个可能的最长回文子序列为 “bbbb”。
示例 2:
输入:
"cbbd"
输出:
2
一个可能的最长回文子序列为 “bb”。
dp 解法
定义状态: dp(i,j)
表示字符串 s
中从位置 i
到 位置 j
的最长子序列长度,其中 i <= j
转移方程:
d p ( i , j ) = { 1 , 当i=j d p ( i + 1 , j − 1 ) + 2 , 当 s . c h a r A t ( i ) = = s . c h a r A t ( j ) m a x { d p ( i + 1 , j ) , d p ( i , j − 1 ) } , 其他情况 dp(i,j)=\begin{cases} 1, & \text {当i=j} \\ dp(i+1, j-1) + 2, & \text {当}s.charAt(i) == s.charAt(j) \\ max \{ dp(i+1, j), dp(i,j-1) \}, & \text {其他情况} \end{cases} dp(i,j)=⎩⎪⎨⎪⎧1,dp(i+1,j−1)+2,max{dp(i+1,j),dp(i,j−1)},当i=j当s.charAt(i)==s.charAt(j)其他情况
这里有一点需要注意,i = j - 1
时,如果有 s.charAt(i) == s.charAt(j)
,那么这时候应该是 dp(i,j) = 2
,但因为初始化时有
d p ( i , j ) = { 1 , 当i=j 0 , 当 i ≠ j dp(i,j)=\begin{cases} 1, & \text {当i=j} \\ 0, & \text {当} i \neq j \end{cases} dp(i,j)={1,0,当i=j当i=j
所以这时候 dp(i,j) = dp(i+1, j-1) + 2
同样满足转移方程,
public int longestPalindromeSubseq(String s) {
if(s==null || s.length()==0)
return 0;
int n = s.length();
char[] str = s.toCharArray();
int[][] dp = new int[n][n];
for(int i = 0; i < n; i++){
dp[i][i] = 1;
}
for(int j = 1; j < n; ++j){
for(int i = j - 1; i >= 0; --i){
if(str[i] == str[j]){
if(i == j - 1){
dp[i][j] = 2;
}else{
dp[i][j] = dp[i + 1][j - 1] + 2;
}
}else{
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return dp[0][n-1];
}
时间复杂度: O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( n 2 ) O(n^2) O(n2)
n
是字符串长度
简省版:
public int longestPalindromeSubseq(String s) {
if (s == null || s.length() == 0)
return 0;
int n = s.length();
char[] str = s.toCharArray();
int[][] dp = new int[n][n];
for (int i = n - 1; i >= 0; --i) {
dp[i][i] = 1;
for (int j = i + 1; j < n; ++j) {
if (str[i] == str[j]) {
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return dp[0][n - 1];
}
递归思想
递归思想的解法跟 dp 解法 几乎相同,
- 当
s.charAt(i) == s.charAt(j)
时,因为要找回文子序列,所以s.charAt(i)
和s.charAt(j)
就属于结果的回文子序列中,继续递归s.charAt(i+1)
和s.charAt(j-1)
之间的子串; - 当
s.charAt(i) != s.charAt(j)
时,就递归考虑s.charAt(i+1)
和s.charAt(j)
之间的子串以及s.charAt(i)
和s.charAt(j-1)
之间的子串
其中 i <= j
,否则直接返回 0
;另外防止重复计算,如果 memo[i][j]
已经计算过,直接返回
public int longestPalindromeSubseq(String s) {
if (s == null || s.length() == 0)
return 0;
int n = s.length();
int[][] memo = new int[n][n];
return helper(s, 0, n - 1, memo);
}
private int helper(String s, int i, int j, int[][] memo) {
if (memo[i][j] != 0) {
return memo[i][j];
}
if (i > j) return 0;
if (i == j) return 1;
if (s.charAt(i) == s.charAt(j)) {
memo[i][j] = helper(s, i + 1, j - 1, memo) + 2;
} else {
memo[i][j] = Math.max(helper(s, i + 1, j, memo), helper(s, i, j - 1, memo));
}
return memo[i][j];
}
时间复杂度: O ( n 2 ) O(n^2) O(n2)
最好情况下 O ( n ) O(n) O(n),最坏情况下 O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( n 2 ) O(n^2) O(n2)