问题引出
给定一个字符串str, 返回str中最长回文子串的长度
str=“123”。其中的最长回文子串“1”或者“2”或者“3”,所以返回1。
str=“abc1234321ab”。其中的最长回文子串“1234321”,所以返回7。
如果str的长度为N,解决原问题的时间复杂度都达到O(N).
输入描述:
输入为一个字符串str
输出描述:
输出一个整数表示最长回文子串的长度
此类问题与KMP算法类似,都是字符串匹配算法,KMP算法利用模式串的next数组,避免重复计算,其核心思想是:
当匹配失败时,下一个开始匹配点不是i+1和j=0,而是:j=next[j-1]
本问题思路
关于回文串,其有两大类类型:
- abcba 即关于中间一个字符对称
- abba 即关于中间两个字符对称
预处理:每一个字符两侧添加’#'字符
abba => #a#b#b#a#
计算的数组为dp[] 其中dp[i]表示第i个字符,向两侧最大可延长长度(当不匹配时不再延长)
可以证明:
dp[]中最大的值,即为原先字符串最大回文串长度
“#1#2#1#” 最大的dp值是以’2’为中心展开即: dp[3]=3 而这正是 “121” 最大回文串长度
"#1#2#2#1#"最大的dp值是以’#'为中心展开即:dp[4]=4 而这正是"1221"最大回文串长度
由此可见,采取预处理,可以使得原先问题转化为处理后字符串延长数组dp的求解。
O(N2)思路
即常规思路,研究每一个位置i 计算dp[i] 那么从i位置不断判断两侧是否相等:s[i-k]==s[i+k]?
该方法时间复杂度为平方级别。
O(N)思路
假设max_right是当前过程,最右边的延长触及位置,其含义是某一个点center,向外拓展最右边到达处
那么,当计算dp[i]时,如果i<max_right 即当前计算位置i包含于之前某个对称区间内,Manacher研究发现,可以利用之前的计算结果:
避免从i-1 i+1两侧开始延长,而是从某个最小的延长位置开始。
最小的延长对比长度为: min(dp[mirror],max_right-i)
证明如下:
①如果其关于center对称位置:dp[mirror]<max_right-i =》那么dp[i]一定等于dp[mirrot]
②如果其关于center对称位置:dp[mirror]>max_right-i =》 那么dp[i]一定大于等于max_right-i
反证法:如果dp[i]小于max_right-i 那么center所在的区间不可能最长延长到max_right处
由此可见,i如果在max_right内部,可以初始化为一个初始值。
如果为②情况,需要继续以min值开始向外延长比对,直到计算出dp[i]。
每一轮计算完dp[i],更新center,max_right 计算下是否为max_ans
代码实现
#include<string>
#include<vector>
#include<iostream>
using namespace std;
int main()
{
string str;
cin>>str;
string s;
s+='#';
for(int i=0;i<str.size();i++)
{
s+=str[i];
s+='#';
}
int len=s.size(); //初始化后的长度
vector<int> dp(len,0); ///dp[i]表示第i个字符 的最大延长 长度
int center=0;
int max_right=0;
int ans=0;
for(int i=0;i<len;i++)
{
if(i<max_right)
dp[i]=min(dp[2*center-i],max_right-i); //在max-right内部,可以避免从1开始延长
else
dp[i]=1;
//不管①② 这里可以统一延长计算
while(i-dp[i]>=0 && i+dp[i]<len && s[i-dp[i]]==s[i+dp[i]]) //延生法
{
dp[i]++;
}
if(i+dp[i]>max_right)
{
max_right=i+dp[i];
center=i; //重新变更拓展点
}
ans=max(ans,dp[i]-1);
}
cout<<ans<<endl;
}
复杂度分析,由于每个位置i的计算,如果发生延长,那么将预先计算好临近区域,即对于每一个位置只计算一次,总的时间复杂度:O(N)
易错点:向外延长时,需要判断下标不越界。或者采取头部辅助一个’@'字符使得计算不会出界。