求所给字符串中最长的回文子串(所给字符串只包含小写字母),并返回这个最长的回文子串
1.动态规划算法(DP)
定义数组 dp:先根据所给字符串 s 的长度来申请创建一个二维的 boolean 型数组 dp 。dp[i][j] 为 true 则表示在字符串 s 中 s[i] 到 s[j] 是字符串 s 的一个回文子串(包含头尾)。
如何进行迭代:
1.若 s[i] != s[j],则 dp[i][j] = false;否则2
2.若 s[i] == s[j],则 dp[i][j] = dp[i+1][j-1];
说明:回文串是对于中心点对称的,所以只需要检验最左(s[i])和最右(s[j])的2个字符串是否相等,若不等则这之间(s[i] 到 s[j])的子串不可能形成回文子串;若相等,则 dp[i][j] 等于dp[i+1][j-1] 的值,完成了迭代。
如何赋予初值:
对于 i >= j ,dp[i][j] = true;否则 dp[i][j] = false;
说明:合法的 i,j 应该满足 i < j,否则没有实际意义,所以我们把所有合法的 i,j 对应的 dp[i][j] 设置为 false,其他的就设置为 true,这种设置初值的方式是DP中常用的。
Java代码如下:
public String longestPalindrome_DP(String s) { //动态规划方法
int len = s.length();
if(len == 0) {
return "";
}
if(len == 1) {
return s;
}
boolean[][] dp = new boolean[len][len];
for(int i = 0; i < len; i++) {
for(int j = 0; j < len; j++) {
if(i >= j) {
dp[i][j] = true;
} else {
dp[i][j] = false;
}
}
}//初始化
int rf = 0, rt = 0;
int k; // 回文子串的长度-1
int maxLen = 1;
for(k = 1; k < len; k++) {
for(int i = 0; i+k<len; i++) {
int j = i + k;
if(s.charAt(i) != s.charAt(j)) {
dp[i][j] = false;
} else {
dp[i][j] = dp[i+1][j-1];
if(dp[i][j]) {
if(k + 1 > maxLen) {
maxLen = k + 1;
rf = i;
rt = j;
}
}
}
}
}
return s.substring(rf, rt+1);
} //longestPalindrome_DP
在leetcode上提交代码时会报 Time Limit Exceeded 的错误,该DP的时间复杂度为O(n*n)。笔者也不知道leetcode上的运行时间限制是多少,最后一个测试用例 s 是1000个‘a’。
2.Manacher算法(马拉车算法)
定义:char 型数组 temp,后续操作是基于数组 temp 的。
定义:int 型数组 p ,p[i] 表示以i为中心的(包含 temp[i] 这个字符)回文串半径长(包含头尾)。数组 p 从 p[2] 开始有实际意义。
首先,Manacher算法提供了一种巧妙地办法,将长度为奇数的回文串和长度为偶数的回文串一起考虑,具体做法是,在原字符串 s 的每相邻两个字符中间插入一个分隔符,同时在首尾也要添加一个分隔符,分隔符的要求是不在原串中出现,一般情况下可以用‘#’号,我们将扩展后的元素放在数组 temp 中。在数组 temp 中我们将 temp[0] 设置为‘@’,防止在后续的操作中越界,见下表。
序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | … |
----| —|---|—|
s | s[0]|s[1]|s[2]|s[3]|s[4]|s[5]|s[6]|…|
temp | @ |#|temp[2]=s[0]|#|temp[4]=s[1]|#|temp[6]=s[2]|…|
>说明:我们可以得出:s[i] = temp[(i+1)*2]
假设现在要求 p[i] ,这意味着 p[2] ~ p[i-1] 的值已经求得。
定义:maxLen 为在数组 temp 中的位置 i 之前所有回文串中能延伸到的最右端的位置,即 maxLen = max{ p[j] + j }( 0 < j < i )。定义 bound 为 maxLen 对应的点,即 maxLen = bound+p[bound]
此时可以分为两种情况来分析:
1. 当前位置 i > maxLen,那么就先设置 p[i] = 1(字符本身是回文子串),然后再用 while 循环来扩展 p[i]。
2. 当前位置 i < maxLen,这种情况又可分为3个小情况,我们先设当前位置 i 相对于位置 bound (见 maxLen 定义)的对称位置为 i’
: A. 当 i’ 的回文串的最左端在 bound 的回文串的最左端的左边(i’-p[i’] < bound-p[bound]),此时 p[i] = p[bound]+bound-i;
B. 当 i’ 的回文串的最左端在 j 的回文串的最左端的右边(i’-p[i’] > bound-p[bound]),此时 p[i] = p[2*bound-i](即 p[i]=p[i’]);
C. 当 i’ 的回文串的最左端与 j 的回文串的最左端相等(i’-p[i’] ==bound-p[bound]),此时先令 p[i] = p[i’],然后用 while 循环来扩展 p[i]。
证明:可根据回文串的对称性来证明 A,B,详细的证明见这里。并且 A,B 这2种情况可以合并为 p[i] = Min(p[2*bound-i], p[bound]+bound-i);(读者可用实际例子求证,详细证明略)
具体代码如下:
public String longestPalindrome_Manacher(String s) { //Manacher 方法
int len=0, bound=0, maxlen=0;
int rf=0, rt=0;
len = s.length();
char[] temp = new char[2*len+3];
int[] p = new int[2*len+2]; //存放回文串的半径,从2开始(包含头尾)
temp[0] = '@';
for(int i = 1; i <= 2*len; i = i + 2) {
temp[i] = '#';
temp[i+1] = s.charAt(i/2);
}
temp[2*len+1] = '#'; // s[i] = temp[(i+1)*2]
temp[2*len+2] = '$';
for(int i = 2; i < 2*len+1; i++) {
if(i < bound+p[bound]) {
p[i] = Math.min(p[2*bound-i], p[bound]+bound-i);
} else {
p[i] = 1;
}
while(temp[i-p[i]] == temp[i+p[i]]) {
p[i]++;
}
if(bound+p[bound] < i + p[i]) {
bound = i;
}
if(maxlen < p[i]) {
maxlen = p[i];
// 对应到 s 中的序号
rf = (i - p[i] + 2) / 2 - 1;
rt = (i + p[i] - 2) / 2 - 1;
}
}
return s.substring(rf, rt+1);
}// longestPalindrome_Manacher
在输入的测试用例 s 是1000个 ‘a’ 的时候,使用动态规划方法,程序的运行时间为 30ms 左右;而使用Manacher算法(马拉车算法),程序的运行时间为 0.6ms 左右。(个人测试结果,仅供参考)