一、引言
给定一个字符串 s
,找到 s
中最长的回文子串。O(n)时间复杂度。
来源:力扣(LeetCode)
二、Manacher算法
1、该算法通过依据回文子串中心字母考虑,所以首先在源字符串中每个字符间插入特殊符号,拼凑为奇数个字符的串。(包括字符串首尾:方便最终截取答案回文子串)
2、该算法流程为单次扫描,每个字符只扫描一次。所以记录如下变量:
(1)maxl:扫描到的最大的右边界下标
(2)maxi:最大右边界所对应的回文子串的中心位置下标
(3)ansi:目前最长回文子串的中心位置下标
同时构建数组armlen[],表示以坐标i为中心的回文子串的臂长(无论是否包括自身都可)
3、随后初始化第一个字符后,遍历每个字符,将其作为子串中心点考虑,对第i个字符有以下的情况:
(1)该字符坐标i在最大右边界maxl的右侧:表示未扫描该字符,使用O(n)算法,以中心扩散的形式扫描该回文子串。
(2)该字符坐标i在最大右边界maxl的左侧:则必定存在i关于maxi的对称点symi,判断对称点symi的回文串臂长:
(i)symi的回文串包含在maxi回文串中,则i的回文串与symi的回文串相同,臂长相同,直接O(1)更新即可。
(ii)symi的回文串超出maxi回文串的边界,则i的回文串也超出maxi的回文串边界,从maxl开始O(n)中心扩散法扫描i的回文串。
4、每遍历一个中心字符后,需要更新maxl和maxi和ansi。
核心思想:
该算法特殊之处在于上述流程中的3.(2),当i小于maxl时,i在maxi的回文串中,所以判断对称点symi的情况来简化i的情况。
三、程序代码
string longestPalindrome(string s)
{
if (s == "")
return s;
// 将字符串拼接位奇数个
string t;
for (int i = 0; i < s.length(); i++)
{
t.push_back('#');
t.push_back(s[i]);
}
t.push_back('#');
int len = t.length();
vector<int> armlen(len, 0); // 以坐标i为中心的回文串的臂长,不算自身
int maxl = 0; // 扫描到的最大右边界
int maxi = 0; // 最大右边界对应的中心坐标
int ansi = 0; // 最长子回文串的中心坐标
for (int i = 1; i < len; i++)
{
if (i >= maxl) // 该中心点在最长右边界右侧,未进行过扫描,O(n)扫描该回文串
{
int l = i - 1;
int r = i + 1;
while (r < len && l >= 0 && t[l] == t[r])
{
armlen[i]++;
l--;
r++;
}
}
else // 该中心点在最长右边界左侧,进行过扫描,找对称点
{
int symi = 2 * maxi - i; // 对称点坐标
if (armlen[symi] >= maxl - i) // 边界以内均为回文,需要继续向外扫描判断
{
armlen[i] = maxl - i;
int l = 2 * i - maxl - 1;
int r = maxl + 1;
while (r < len && l >= 0 && t[l] == t[r])
{
armlen[i]++;
l--;
r++;
}
}
else // 回文串在边界内,直接匹配
{
armlen[i] = armlen[symi];
}
}
// 维护最大臂长,及答案中心点坐标
if (i + armlen[i] > maxl)
{
maxl = i + armlen[i];
maxi = i;
}
if (armlen[i] > armlen[ansi])
{
ansi = i;
}
}
string ans = s.substr((ansi-armlen[ansi])/2, armlen[ansi]);
return ans;
}