前(che)言(dan)
最近做了很多字符串的题目,其中有一些就要用到Manacher算法,故花了一上午去学习学习。
网上题解很多,不保证大家看一篇就能懂,建议多看几篇。
那么,下面这些,就当做是我的学习笔记与对算法的理解吧!
预备知识
子串:字符串中连续的一段字符序列,如的子串有
,
,
,
,
,
.
回文串:假设字符串长度为
,,假设字符串位置从
开始,对于任意
,都有
,如
就是一个回文串。
回文子串:满足回文串条件的子串。
有何用?
求解一个字符串中最长的回文子串,洛谷有模板题:https://www.luogu.org/problemnew/show/P3805
尝试求解
枚举所有子串再检查还不行吗?时间复杂度为.
能不能更快一点?
好吧,我们容易发现,回文串是左右对称的。
我们先假设回文串长度为奇数,那么必定存在一个位置使得
左右子串对称,我们枚举这个
,找出关于
中心对称的最长回文子串。
但是假设最长回文串为偶数呢?
我们在开头,结尾,以及相邻两个字符间插入字符(其它的也行,只要不出现在原串中),这样它的长度就变为了奇数,然后按照上述做法,最后把长度对
整除即可。
比如:;
(以第
个
为中心的最长回文串长度为
,最长回文串为
)
.
显然以为回文中心的回文子串最长,那么原串最长回文子串为
。
时间复杂度为.
能不能再快一点?
好了,正文来了。(为了不至于您看文章的过程中犯困,我把重点加粗标记了起来)
我们发现,虽然上述算法已经比较优了,但还是有很多子串被重复枚举。
引入数组,其中
表示以
为回文中心向右延伸的最长长度。
比如说对于,
,因以第二个
为回文中心的最长回文子串为
,故从第二个
向右可延伸最大长度为
,即原串第
第
位。
引入,其中
表示当前所有回文子串延伸到的最远右端点,
为其对应的回文串的回文中心。
这就要求,回文中心还是要存在的,偶数长度的回文串还是不能有的。
那么就像上一种方法那样,无脑插入字符,可以证明,无论是奇数还是偶数长度的串,都可以通过上述方法变为奇数长度的串。
然后就是比较重要的一条性质:.
首先有什么意义呢?列举几个数据发现,它是
关于
中心对称的位置;那
呢?其实就是
与
之间的字符串长度.
令,
,则:
之间的字符与
之间的字符是相同的。
把代入,则
与
,由定义知
,故:
.
最终结论:与
的字符是相同的。
又,故:
若,字符相同的这段区间被以
为回文中心的字符串所包含,那么由上述推出的对称相等,以
为中心的回文串长度为
,即
;
若,多出的部分虽然相等但不回文,仍取较小值,即
。
这样就避免了重复子串的枚举,时间效率大大提高。
之后的过程就简单了,通过一行,就可轻松把子串向左右两边延展;同时,不要忘了更新
和
。
最终的结果就为,这是因为我们在预处理时在相邻字符间插入了新的字符,故
的值,本应表示回文串长度
之后的一半,成为了回文串的长度
(
是因为回文中心被多算了一遍)。
因为不会枚举到重复的字串,最终均摊下来的时间复杂度为.
参考代码:
#include<cstdio>
#include<algorithm>
#include<iostream>
using namespace std;
int n,cnt;
char c[12000000];
char str[23000000];
int nxt[23000000];
int main()
{
char s=getchar();
while (s>='a'&&s<='z')
{
c[++n]=s;
s=getchar();
}
for (int i=1;i<=n;i++)
{
str[++cnt]='#';
str[++cnt]=c[i];
}
str[++cnt]='#';
int now=0;
int r=0;
int ans=0;
for (int i=1;i<=cnt;i++)
{
if (i<=r) nxt[i]=min(nxt[now*2-i],r-i+1);
while (str[i+nxt[i]]==str[i-nxt[i]]&&i>nxt[i]) nxt[i]++;
if (nxt[i]+i-1>r)
{
r=nxt[i]+i-1;
now=i;
}
if (nxt[i]-1>ans)
ans=nxt[i]-1;
}
printf("%d",ans);
}