上海自来水来自海上
山东落花生花落东山
0.问题描述
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: “aba” 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
1.动态规划
首先容易想到的一个方法就是暴力检索,对于每一个可能存在的子串进行穷举判断是否是回文子串。但这个方法的复杂度是O(n^3)。
为了减少暴力检索里面造成的许多无用检索,自然而然的我们就想到的是动态规划的方法。使用一个数组dp,dp[i][j]来表示s[i]到s[j]之间的子串是否为回文子串。如果是的话,那么如果这个子串前后的两个字符也是相同的,毫无疑问这个是更长的最长子串。
时间复杂度O(n^2), 空间O(n^2),动态规划方程如下:
-
dp[i][j] 表示子串s[i…j]是否是回文
-
初始化:dp[i][i] = true (0 <= i <= n-1); dp[i][i-1] = true (1 <= i <= n-1); 其余的初始化为false
-
dp[i][j] = (s[i] == s[j] && dp[i+1][j-1] == true)
在动态规划中保存最长回文的长度及起点即可
class Solution {
public:
string longestPalindrome(string s) {
//const len = s.length();
//bool dp[len][len];
bool dp[1000][1000];
int k,i;
int lres=0,rres=0;
memset(dp,0,sizeof(dp));
if(s.length()<=1){
return s;
}
dp[0][0]=true;
for(i=1;i<s.length();i++){
dp[i][i]=true;
dp[i][i-1]=true;
}
for(k=2;k<=s.length();k++){
for(i=0;i<=s.length()-k;i++){
if(s[i]==s[k+i-1]&&dp[i+1][i+k-2]){
dp[i][i+k-1]=true;
if(rres-lres+1<k){
rres=i+k-1;
lres=i;
}
}
}
}
return s.substr(lres,rres-lres+1);
}
};
这个方法仍然是在平方的时间内完成的检索,因为判断是否为最长是通过一个一个长度进行检索的,不能在一个线性时间内同时完成检索和长度的判断。
2.中心扩展
事实上,只需使用恒定的空间,我们就可以在 O(n^2)的时间内解决这个问题。
我们观察到回文中心的两侧互为镜像。因此,回文可以从它的中心展开,并且只有 2n - 1个这样的中心。
你可能会问,为什么会是2n−1 个,而不是 n 个中心?原因在于所含字母数为偶数的回文的中心可以处于两字母之间(例如 “abba” 的中心在两个{‘b’}‘b’ 之间)。
我们查找两次,查找一遍奇数长度的,再查找一遍偶数长度的。把每一个字母为中心的最长回文字符串都找到,然后在结果的记忆中判断大小。
注意for语句的几个判断条件的范围。
class Solution {
public:
string longestPalindrome(string s) {
const int len = s.size();
if(len <= 1)return s;
int start, maxLen = 0;
for(int i = 1; i < len; i++){
int low = i-1, high = i;
while(low >= 0 && high < len && s[low] == s[high]){
low--;
high++;
}
if(high - low - 1 > maxLen) {
maxLen = high - low -1;
start = low + 1;
}
low = i- 1; high = i + 1;
while(low >= 0 && high < len && s[low] == s[high]){
low--;
high++;
}
if(high - low - 1 > maxLen){
maxLen = high - low -1;
start = low + 1;
}
}
return s.substr(start, maxLen);
}
};
此次方法显著快于第一个方法。
3.Manacher算法
搬运大佬的算法,强无敌。原文链接
Manacher算法,时间复杂度O(n), 空间复杂度O(n)。强无敌。
该算法首先对字符串进行预处理,在字符串的每个字符前后都加入一个特殊符号,比如字符串 abcd 处理成 #a#b#c#d#,为了避免处理越界,在字符串首尾加上不同的两个特殊字符(c类型的字符串尾部不用加,因为自带‘\0’),这样预处理后最终变成$#a#b#c#d#^,经过这样处理后有个好处是原来的偶数长度和奇数长度的回文在处理后的字符串中都是奇数长度。
假设处理后的字符串为s,
对于已经预处理好的字符串我们用数组p[i]来记录以字符S[i]为中心的最长回文子串向左/右扩张的长度(包括S[i]),以字符串“12212321”为例,p数组如下
s: $ # 1 # 2 # 2 # 1 # 2 # 3 # 2 # 1 # ^
p: 1 2 1 2 5 2 1 4 1 2 1 6 1 2 1 2 1
可以看出,P[i]-1正好是原字符串中回文串的总长度, 如果p数组已知,遍历p数组找到最大的p[i]就可以求出最长回文的长度,也可以求出回文的位置
下面给出求p[]数组的方法:
设id是当前求得的最长回文子串中心的位置,mx为当前最长回文子串的右边界(回文子串不包括该右边界),即mx = id + p[id]。记j = 2*id – i ,即 j 是 i 关于 id 的对称点。
1、 当i < mx 时,如下图。此时可以得出一个非常神奇的结论p[i] >= min(p[2*id - i], mx - i),下面我们来解释这个结论
如何根据p[j]来求p[i]呢,又要分成两种情况
(1.1)当mx – i > p[j], 这时候以S[j]为中心的回文子串包含在以S[id]为中心的回文子串中,由于 i 和 j 对称,以S[i]为中心的回文子串必然包含在以S[id]为中心的回文子串中,所以 P[i] 至少等于 p[j], 后面的再继续匹配。如下图
注:这里其实p[i]一定等于p[j],后面不用再匹配了。因为如果p[i]后面还可以继续匹配,根据对称性,p[j]也可以继续扩展了
(1.2)当mx – i <= p[j], 以S[j]为中心的回文子串不完全包含于以S[id]为中心的回文子串中,但是基于对称性可知,下图中两个绿框所包围的部分是相同的,也就是说以S[i]为中心的回文子串,其向右至少会扩张到mx的位置,也就是说 P[i] 至少等于 mx - i,至于mx之后的部分是否对称,就只能老老实实去匹配了。
注:如果mx – i < p[j] ,这时p[i]一定等于mx - i, 因为如果p[i]在mx之后还可以继续匹配,根据对称性,mx之后匹配的点(包括mx)一定会出现在my的前面,这说明p[id]也可以继续扩展了
2、当i >= mx, 无法对p[i]做更多的假设,只能p[i] = 1,然后再去匹配
算法复杂度分析:根据斜体字部分的注释,只有当mx-i = p[j]时 以及 i > mx时才要扩展比较,而mx也是在不断扩展的,总体而言每个元素比较次数是n的线性关系,所以时间复杂度为O(n)
class Solution {
public:
string preProcess(const string &s)
{
int n = s.size();
string res;
res.push_back('$');
res.push_back('#');
for(int i = 0; i < n; i++){
res.push_back(s[i]);
res.push_back('#');
}
res.push_back('^');
return res;
}
string longestPalindrome(string s) {
const int len = s.size();
if(len <= 1)return s;
string str = preProcess(s);
int n = str.size(), id = 0, mx = 0;
vector<int>p(n, 0);
for(int i = 1; i < n-1; i++){
p[i] = mx > i ? min(p[2*id-i], mx-i) : 1;
while(str[i+p[i]] == str[i-p[i]]){
p[i]++;
}
if(i + p[i] > mx){
mx = i + p[i];
id = i;
}
}
int maxLen = 0, index = 0;
for(int i = 1; i < n-1; i++){
if(p[i] > maxLen) {
maxLen = p[i];
index = i;
}
}
return s.substr((index - maxLen)/2, maxLen-1);
}
};
效率非常高,leetcode是8ms。
4.Java解法
Java我们采用动态规划的思想进行求解。
class Solution {
public String longestPalindrome(String s) {
int n = s.length();
String res = "";
boolean[][] dp = new boolean[n][n];
for(int i = 0 ;i < n; i++){
for(int j = i; j >= 0 ;j --){
if(s.charAt(i) == s.charAt(j) && ( i - j < 2 || dp[i-1][j+1]))
dp[i][j] = true;
if (dp[i][j] && (i - j + 1 > res.length())){
res = s.substring(j,i+1);
}
}
}
return res;
}
}
5.Python解法
Python我们还是采用马拉车的算法。
class Solution:
def longestPalindrome(self, s: str) -> str:
l=len(s)
if l<=1: return s
str1=[]
str1.append('$')
str1.append('#')
for i in range(0,l):
str1.append(s[i])
str1.append('#')
str1.append('^')
p=[]
idd=mx=0
for i in range(1,l-1):
p[i]=min(p[2*idd-1],mx-i) if mx>i else 1
while str1[i+p[i]] == str1[i-p[i]]:
p[i]+=1
if i+p[i]>mx:
mx=i+p[i]
idd=i
maxlen=index=0;
for i in range(1,n-1):
if p[i]>maxlen:
maxlen=p[i]
index=i
return s.substr((index - maxLen)/2, maxLen-1);