第五题:最长回文子串
- 给你一个字符串 s,找到 s 中最长的回文子串
- 提示:回文串(palindromic string)是指这个字符串无论从左读还是从右读,所读的顺序是一样的;简而言之,回文串是左右对称的
示例 1:
- 输入:s = "babad"
- 输出:"bab"
- 解释:"aba" 同样是符合题意的答案
示例 2:
- 输入:s = "cbbd"
- 输出:"bb"
示例 3:
- 输入:s = "a"
- 输出:"a"
示例 4:
- 输入:s = "ac"
- 输出:"a"
提示:
- 1 <= s.length <= 1000
- s 仅由数字和英文字母(大写和/或小写)组成
引入Manacher算法
博主也不知道Manacher 算法,所以参考了几篇文章,比较好的是:https://www.jianshu.com/p/eb1bfcb0d9e2
维基百科中对于 Manacher 算法是这样描述的:
[Manacher(1975)] 发现了一种线性时间算法,可以在列出给定字符串中从字符串头部开始的所有回文。并且,Apostolico, Breslauer & Galil (1995) 发现,同样的算法也可以在任意位置查找全部最大回文子串,并且时间复杂度是线性的。因此,他们提供了一种时间复杂度为线性的最长回文子串解法。替代性的线性时间解决 Jeuring (1994), Gusfield (1997)提供的,基于后缀树(suffix trees)。也存在已知的高效并行算法。
在还没有实现算法之前,我们先要弄清楚算法的运行流程,即给我们一个具体的字符串,我们通过稿纸演算的方式,应该如何得到给定字符串的最长子回文串。
理解 Manacher 算法最好的办法,其实是根据一些关于 Manacher 算法的文章,自己写写画画,最好能产生一些输出,画画图,举一些具体的例子,这样 Manacher 算法就不难搞懂了。
Manacher 算法本质上还是中心扩散法,只不过它使用了类似 KMP 算法的技巧,充分挖掘了已经进行回文判定的子串的特点,使得算法高效。
回文串可分为奇数回文串和偶数回文串,它们的区别是:奇数回文串关于它的“中点”满足“中心对称”,偶数回文串关于它“中间的两个点”满足“中心对称”。我们在具体判定一个字符串是否是回文串的时候,常常会不自觉地考虑到它们之间的这个小的差别。
第 1 步:预处理,添加分隔符
我们先给出具体的例子,看看如何添加分隔符。
例1:给字符串
"bob"
添加分隔符"#"
。答:
"bob"
添加分隔符"#"
以后得到:"#b#o#b#"
。
再看一个例子:
例2:给
"noon"
添加分隔符"#"
。答:
"noon"
添加分隔符"#"
以后得到:"#n#o#o#n#"
。
我想你已经看出来分隔符是如何添加的,下面是 2 点说明。
1、分隔符是字符串中没有出现过的字符,这个分隔符的种类只有一个,即你不能同时添加 "#"
和 "?"
作为分隔符;
2、在字符串的首位置、尾位置和每个字符的“中间”都添加 1个这个分隔符,可以很容易知道,如果这个字符串的长度是 len
,那么添加的分隔符的个数就是 len + 1
,得到的新的字符串的长度就是 2len + 1
,显然它一定是奇数。
为什么要添加分隔符?
1、首先是正确性:添加了分隔符以后的字符串的回文性质与原始字符串是一样的。
2、其实是避免奇偶数讨论,对于使用“中心扩散法”判定回文串的时候,长度为奇数和偶数的判定是不同的,添加分隔符可以避免对奇偶性的讨论。
对于一个串str长度为n,有n-1个空格,首位有两个,对这些空处理,长度变成2n+1 (也就是不管奇偶扩展后肯定是奇数)
第 2 步:得到 p 数组
首先,我们先来看一下如何填表。以字符串 "abbabb"
为例,说明如何手动计算得到 p 数组。假设我们要填的就是下面这张表。
char | # | a | # | b | # | b | # | a | # | b | # | b | # |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
p | 1 | 2 | 1 | 2 | 5 | ||||||||
p-1 |
第 1 行 char 数组:这个数组就是待检测字符串加上分隔符以后的字符构成的数组。
第 2 行 index 数组:这个数组是索引数组,我们后面要利用到它,填写即索引从 0 开始写就好了。
下面我们来看看 p 数组应该如何填写。首先我们定义回文半径。
回文半径:以 char[i]
作为回文中心,同时向左边、向右边进行扩散,直到不能构成回文串或者触碰到边界为止,能扩散的步数 + 1 ,即定义为 p 数组索引的值,也称之为回文半径。
以上面的例子,我们首先填。p[0]
,以 char[0] = '#'
为中心,同时向左边向右扩散,走 1 步就碰到边界了,因此“能扩散的步数”为0,“能扩散的步数 + 1 = 1”,因此 p[0] = 1
;
char | # | a | # | b | # | b | # | a | # | b | # | b | # |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
p | 1 | ||||||||||||
p-1 |
下面填写 p[1]
,以 char[1] = 'a'
为中心,同时向左边向右扩散,走 1 步,左右都是 "#"
,构成回文子串,于是继续同时向左边向右边扩散,左边就碰到边界了,因此“能扩散的步数”为1,“能扩散的步数 + 1 = 2”,因此 p[1] = 2
;
char | # | a | # | b | # | b | # | a | # | b | # | b | # |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
p | 1 | 2 | |||||||||||
p-1 |
下面填写 p[2]
,以 char[2] = '#'
为中心,同时向左边向右扩散,走 1 步,左边是 "a"
,右边是 "b"
,不匹配,因此“能扩散的步数”为 ,“能扩散的步数 + 1 = 1”,因此 p[2] = 1
;
char | # | a | # | b | # | b | # | a | # | b | # | b | # |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
p | 1 | 2 | 1 | ||||||||||
p-1 |
下面填写 p[3]
,以 char[3] = 'b'
为中心,同时向左边向右扩散,走 1 步,左右两边都是 “#”
,构成回文子串,继续同时向左边向右扩散,左边是 "a"
,右边是 "b"
,不匹配,因此“能扩散的步数”为1,“能扩散的步数 + 1 = 2”,因此 p[3] = 2
;
char | # | a | # | b | # | b | # | a | # | b | # | b | # |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
p | 1 | 2 | 1 | 2 | |||||||||
p-1 |
下面填写 p[4]
,以 char[4]='#'
为中心,同时向左边向右扩散,可以知道可以同时走 4 步,左边到达边界,因此“能扩散的步数”为4,“能扩散的步数 + 1 = 5”,因此 p[4] = 5
。
char | # | a | # | b | # | b | # | a | # | b | # | b | # |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
p | 1 | 2 | 1 | 2 | 5 | ||||||||
p-1 |
分析到这里,后面的数字不难填出,最后写成如下表格:
char | # | a | # | b | # | b | # | a | # | b | # | b | # |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
p | 1 | 2 | 1 | 2 | 5 | 2 | 1 | 6 | 1 | 2 | 3 | 2 | 1 |
p-1 |
p-1 数组很简单了,把 p 数组的数 -1 就行了。实际上直接把能走的步数记录下来就好了。不过就是为了给“回文半径”一个定义而已。
于是我们得到如下表格:
char | # | a | # | b | # | b | # | a | # | b | # | b | # |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
p | 1 | 2 | 1 | 2 | 5 | 2 | 1 | 6 | 1 | 2 | 3 | 2 | 1 |
p-1 | 0 | 1 | 0 | 1 | 4 | 1 | 0 | 5 | 0 | 1 | 2 | 1 | 0 |
于是:数组 p -1 的最大值就是最长的回文子串,可以在得到 p 数组的过程中记录这个最大值,并且记录最长回文子串。
解题:
/**
* @file 5.cpp
* @author HarkerYX
* @brief 最长回文子串
* @version 0.1
* @date 2021-04-27
*
* @copyright Copyright (c) 2021
*
*/
/*
第五题:最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串
解题思路:
Manacher 算法解决 奇偶回文的不确定性,对于一个串str长度为n,有n-1个空格,首位有两个,对这些空处理,长度变成2n+1
再者就是知道了扩展后的中心位置如何推算出原始位置
*/
#include <stdio.h>
#include <windows.h>
char * createManacherStr(char *s)
{
int len = strlen(s);
//不去判断长度了,就当不是空字符串
char *newstr = (char *)malloc(len * 2 + 2);
char *p = newstr;
*newstr++ = '#';
while(*s != NULL){
*newstr++ = *s;
*newstr++ = '#';
s++;
}
*newstr = '\0';
return p;
}
/**
* @brief
*
* @param org
* @return char*
*/
char * longestPalindrome(char* org){
char *s = NULL;
//创建扩展字符串
s = createManacherStr(org);
printf("newsStr = %s ,len = %d \n",s,strlen(s));
//记录最终最长回文长度
int maxPalindromeLen = 0;
//Manacher扩展后的长度
int len = strlen(s);
//记录找到的最大回文的移动位置
int maxPos = 0;
//每次位移时候算出来的最大回文长度
int tmpMaxlen = 0;
for (int i = 0; i<len ;i++){
tmpMaxlen = 0;
// 判断长度中心点,位置在左边的情况时,左边扩展肯定要小于右边的长度,那就看左边就行
if(i <= (len/2+1))
{
//当前的位置能往左移动的步数
int MaxleftPos = i;
for (int j = 0; j <= MaxleftPos; j++)
{
// if(i==4){
// printf("%c,%c\n",s[i - j],s[i + j]);
// }
if(s[i-j] != s[i+j])
break;
tmpMaxlen++;
}
}
else if(i > (len/2+1)){
//当前的位置能往右移动的步数
int MaxRightPos = len - i;
for (int j = 0; j <= MaxRightPos; j++)
{
if(s[i-j] != s[i+j])
break;
tmpMaxlen++;
}
}
// 每次移动判断出来的最大回文判断后给最终回文
maxPalindromeLen = maxPalindromeLen > tmpMaxlen ? maxPalindromeLen : tmpMaxlen;
// 如果 if(tmpMaxlen > maxPalindromeLen) 就得到第一个最大位置 ,>=就得到最后的一个最大位置
if(tmpMaxlen >= maxPalindromeLen)
maxPos = i;
}
//为什么 -1 ,看Manacher介绍最后就知道了
maxPalindromeLen -= 1;
printf("最大回文长度 = %d \n",maxPalindromeLen);
printf("Manacher扩展后的字符串中的位置: %d\n",maxPos);
// 得到最大位置在 maxPos ,结合最大的回文推算出 扩展字符串从什么位置开始
int org_pos = (maxPos - 1) / 2;
int beginPos = org_pos - (maxPalindromeLen - 1) / 2;
printf("原字符串的位置: %d \n", beginPos);
char *tmpS = (char *)malloc(maxPalindromeLen * sizeof(char) + 1);
memcpy(tmpS, org + beginPos, maxPalindromeLen);
tmpS[maxPalindromeLen] = '\0';
printf("最大回文原字符串: %s \n", tmpS);
return tmpS;
}
int main(void)
{
// char *s ="a";
// char *s ="ac";
// char *s ="cbbd";
char *s = "babad";
longestPalindrome(s);
return 0;
}
newsStr = #b#a#b#a#d# ,len = 11
最大回文长度 = 3
Manacher扩展后的字符串中的位置: 5
原字符串的位置: 1
最大回文原字符串: aba