给出一个字符串,要求字符串的最长回文子串,怎么办?
#define _CRT_SECURE_NO_WARNINGS #include <cstdio> #include <cstring> #include <cstdlib> bool isPalindrome(const char* str, int n) { int j = 0, upper = (n + 1) / 2; while (j < upper) { if (str[j] != str[n - j - 1]) return false; j++; } return true; } char* getLongestPalind(const char* str) { int len = (int)strlen(str), ans = 0; int retp = 0, retq = 0, k = 0; char* ret = (char*)malloc(sizeof(char) * len); for (int i = 0; i < len; ++i) { for (int j = i; j < len; ++j) { if (isPalindrome(str + i, j - i + 1)) { if (ans < j - i + 1) { ans = j - i + 1; retp = i, retq = j; } } } } for (int s = retp; s <= retq; ++s) ret[k++] = *(str + s); ret[k] = '\0'; return ret; } int main() { char s[100] = { '\0' }; scanf("%s", s); printf("%s\n", getLongestPalind(s)); return 0; }
简单测试一个数据:
再去leetcode上找一个题来测试一下:
在这个问题里就会超时~~
#define _CRT_SECURE_NO_WARNINGS #include <cstdio> #include <cstring> #include <cstdlib> char* getLongestPalind(char* str) { int len = (int)strlen(str), ans = 0, l = 0, r = 0, t = 0; char* ret = (char*)malloc(sizeof(char) * len + 1); if (0 == len || 1 == len) return str; for (int i = 0; i < len; ++i) { int j = 0; while (i - j >= 0 && i + j < len && str[i - j] == str[i + j]) j++; if (ans < j) { ans = j; l = i - j + 1, r = i + j - 1; } } for (int k = l; k <= r; ++k) ret[t++] = str[k]; ret[t] = '\0'; return ret; } int main() { char s[100] = { '\0' }; scanf("%s", s); printf("%s\n", getLongestPalind(s)); return 0; }
这下再去交一下,看看结果哈:
这样就AC了哈~~
这种算法很简单,复杂度是平方级别~
思想就是选定一个中心,然后向两边扩展,求得最长的回文子串~
首先,大家伙看完上面那一份代码之后,就会晓得:在处理回文子串的时候,奇数串和偶数串处理起来是不一样的,因为上面那个做法是枚举子串的中心字符,然后两边扩展。但是偶数串是没有中心字符的,它的中心是长度除以二后,的左右两个字符,那样处理是不方便的,所以我们先统一两者,给字符串填充相同,且不会出现在原串中的字符 ‘#’ ,但是上面那份代码在扩展的时候,还是要注意边界,给了些判断条件,防止越界~所以我们干脆再给头尾加上与原串字符、’#’ 都不相同的字符,比如 ‘@’ 、 ‘$’ 等等!
void padding(char* s){ int len = strlen(s), i; pad[0] = '$'; // pad数组是事先声明好了的~最好是静态初始化哦 pad[1] = '#'; for(i = 0;i < len;++i){ pad[2 * i + 2] = s[i]; pad[2 * i + 3] = '#'; } pad[2 * len + 2] = '@'; k = 2 * len + 2; pad[k + 1] = '\0'; // 别忘了字符串要以 '\0' 结尾哦~ }
在manacher算法中,我们需要维护一个数组 p[] ,设定 p[i] 的含义是:以 i 为中心的最长回文半径的长度~设定右边界是rx,初始情况rx = 0,由于之前字符串进行了填充,于是长度由 length = 2 * length + 1(刨去了首位防越界的限定符!),所以这个 p[i] - 1
就会是 length * 2,那么也就是原串回文半径的两倍,这个两倍要注意重复计算了回文中心的字符,于是这个值减1就是最长回文串的长度了~
大家看到了,能够维护出 p[] 就能得到每个回文子串的长度!!!那么,如何维护它就是技术的重点!
上面这个图是举的一个求 p[] 的示例,我们先来分析一下这个 p[] 如何求得!
对于任意一个位置 i ,假设 j 是 i 关于 id 的对称点,也就是说j = 2 * id - 1
,如果 i <= mx
,那么对应的 j 也就会在 mx 的对称点的右侧。现在来分类讨论:
此时,设 L = 以 j 为中心的最长回文子串的左边界 - 1,R = 以 j 为中心的最长回文子串的右边界 + 1
肯定有Str[L] != Str[R]
,否则最长回文子串的左边界就会是 L 了!因为此时必有: p[i] >= p[j]
,如果p[i] 会比 p[j] 大,那么以 i 为中心的最长回文子串的右边界会和它的左边界相同。又因为以 j 为中心的回文子串的左边界 + 1 会和 p[j] 的右边界相同,所以如果 p[i] > p[j]
,就会让 L != 以 j 为中心的最长回文子串的左边界 - 1,这就矛盾了!!!
所以此时:p[i] = p[j]
首先,如果 j 为中心的最长回文子串左边界超过了 mx 的对称点,举个例子:
e [ a b c b a e f (g) f e a b c b a] y
这里,我们的回文串是以 ‘g’ 为中心的,假设 Str[j] = Str[i] = 'c'
,则此回文子串左侧的 ‘e’ 就有可加入 j 为中心的最长回文子串。但是呢?以 i 为中心的最长回文子串就不可以加入右侧的 ‘y’ !所以此时,p[i] = mx - i;
!
但是如果 j 为中心的最长回文子串左边界恰恰在 mx 的对称点呢?比如:
y [ a b c b a e f (g) f e a b c b a] e
像这种情况呢???那么初始情况就只能是:p[i] = p[j];
!大家可以发现,这两种情况啊在不确定的时候,就是取两者的最小值的!!!于是得到结论:
if(i <= mx) p[i] = min(p[2 * id - i], mx - i);
这id啊,就是上一步得到的最长回文子串的中心~
这个就很简单啦~只需要给 p[i] = 1
初始化就好!因为在初始情况下,我们认为一个字符本身就是回文串~
我们知道,在第二点里,以 i 为中心的最长回文子串右边界是有可能扩展的,所以需要加个循环判断一下,考虑可能会增加 p[i] 的值。其实这个每种情况都需要考虑,所以要加上:
while (pad[i - p[i]] == pad[i + p[i]]) p[i]++;
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <string.h> char pad[2005], str[1005]; int k, p[2005]; int min(int a, int b) { return a < b ? a : b; } void padding(char* s) { int len = strlen(s), i; pad[0] = '$'; pad[1] = '#'; for (i = 0; i < len; ++i) { pad[2 * i + 2] = s[i]; pad[2 * i + 3] = '#'; } pad[2 * len + 2] = '@'; k = 2 * len + 2; pad[k + 1] = '\0'; } char* manacher(char* s) { if (strlen(s) == 0 || strlen(s) == 1) return s; int i, rx = 0, id, ans = 0, pos = 0, len = 0, n = 0; padding(s); for (i = 1; i < k; ++i) { if (rx >= i) p[i] = min(rx - i, p[2 * id - i]); else p[i] = 1; while (pad[i - p[i]] == pad[i + p[i]]) p[i]++; if (i + p[i] > rx) { rx = i + p[i]; id = i; } if (ans < p[i]) { ans = p[i]; pos = i, len = p[i] - 1; } } for (int t = pos - len; t < pos + len + 1; ++t) if ('#' != pad[t]) str[n++] = pad[t]; str[n] = '\0'; return str; } int main() { char t[100] = { '\0' }; scanf("%s", t); printf("%s\n", manacher(t)); return 0; }
提交到leetcode上,看看这次的效果如何:
这下效果很明显啊,从40ms到4ms,这是因为复杂度也下降了一个数量级啊!哈哈,再练一道模板题~
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <string.h> const int maxn = 2.2e7 + 5; char pad[maxn], str[maxn / 2]; int k, p[maxn]; int min(int a, int b) { return a < b ? a : b; } void padding(char* s) { int len = strlen(s), i; pad[0] = '$'; pad[1] = '#'; for (i = 0; i < len; ++i) { pad[2 * i + 2] = s[i]; pad[2 * i + 3] = '#'; } pad[2 * len + 2] = '@'; k = 2 * len + 2; pad[k + 1] = '\0'; } int manacher(char* s) { if (!s) return 0; if (strlen(s) == 1) return 1; int i, rx = 0, id, ans = 0, pos = 0, len = 0, n = 0; padding(s); for (i = 1; i < k; ++i) { if (rx >= i) p[i] = min(rx - i, p[2 * id - i]); else p[i] = 1; while (pad[i - p[i]] == pad[i + p[i]]) p[i]++; if (i + p[i] > rx) { rx = i + p[i]; id = i; } if (ans < p[i]) ans = p[i]; } return --ans; } int main() { scanf("%s", str); printf("%d\n", manacher(str)); return 0; }
这个题要求所有回文子串的个数,这个其实本质还是Manacher算法,因为每次维护起来的 p[i] 都是以当前第 i 个位置为中心的最长回文串的长度 + 1,因为我们知道,这个最长回文串左右各减少一个,它仍然是回文串!!于是我们只需要把 (p[i] - 1) / 2
就可以得到所有长度大于1 的回文串的个数,但是要知道一个字符也是一个回文串~所以必须向上取整,也就是:p[i] / 2
!!
char pad[2005]; int k, p[2005]; int min(int a, int b){ return a < b ? a : b; } void padding(char* s){ int len = strlen(s), i; pad[0] = '$'; pad[1] = '#'; for(i = 0;i < len;++i){ pad[2 * i + 2] = s[i]; pad[2 * i + 3] = '#'; } pad[2 * len + 2] = '@'; k = 2 * len + 2; pad[k + 1] = '\0'; } int countSubstrings(char* s){ int i, rx = 0, id, ans = 0; padding(s); for(i = 1;i < k;++i){ if(rx > i) p[i] = min(rx - i, p[2 * id - i]); else p[i] = 1; while(pad[i - p[i]] == pad[i + p[i]]) p[i]++; if(i + p[i] > rx){ rx = i + p[i]; id = i; } ans += p[i] / 2; } return ans; }
关于Manacher算法的基础讲解和代码实现,以及一个基础应用就讲到这里了。由于本人能力有限,如有不正确或者有瑕疵的地方,还请指正!!谢谢!