回文串介绍
定义
- 若一个字符串和它的逆串相同,例如$aba$,$acbca$,$acca$,那么满足这个性质的字符串被称为回文串。
性质
- 对称性: $S$ 总是满足 $S_i=S_{n-i+1}(i \le n)$。
- 奇偶性:回文串可以分为奇数长度和偶数长度两种类型,长度为偶数的回文串的对称中心是一个空字符,而奇数长度的字符串对称中心是第 $S_{\frac{n+1}{2}}$ 个字符。
- 一个字符串本质不同的回文串个数不超过 $O(n)$。
- 二分性:回文串的长度是支持二分的,因为回文串在两边删除相等的字符,仍然是一个回文串。
例题1
- 给定一个长度不超过1000的字符串 $S$,输出最长的回文串长度
中心枚举
由于回文串都是中心对称的,因此我们可以枚举所有可能的中心位置,然后不断向两边添加相同字符,最后记录得到的最长的串。
我们发现,奇数长度的字符串中心非常好枚举,但是偶数长度的就有点点麻烦,因为偶数长度的中心不是一个字符而是在两个字符之间,因此我们考虑将这个字符串做一个预处理,在字符串的开头,结尾,每两个字符之间都添加一个原本字符串中不包含的特殊符号,使得字符串变成如下的形式
经过预处理后,原串中长度为偶数的串对称中心就变为了"#"字符,稍微分析可以发现,预处理后的字符串不包含长度为偶数的回文串。由此,我们可以枚举回文串的中心是哪一个字符,然后不断的向两边扩展,并且记录最长的长度 $len$,但是注意到,因为新串当中添加了字符"#",因此原串的最长回文串长度为$len$ 减去其中"#"字符的个数。总体时间复杂度 $O(n^2)$
例题2
- 给定一个长度不超过$10^6$的字符串 $S$,输出最长的回文串长度
Manacher算法
预处理
- 原来的中心枚举算法效率并不高,1975年,一个叫Manacher的人发明了一个算法---Manacher算法,利用了回文串的对称性质,将上述中心枚举算法的时间复杂度从 $O(n^2)$ 优化为了 $O(n)$
- 利用和中心对称方法相同的预处理方法,将字符串里所有的奇数长度回文串变成了偶数长度。
回文串半径
- 定义数组 $p$,$p_i$ 表示以字符串的第 $i$ 位为中心的最长回文串半径
如果求得 $p$,那么很快可以得到答案,现在考虑如何快速求 $p$
对称性
- 在原来的中心枚举算法当中,我们是在当前位置 $i$,利用双指针向两边扩展,现在定义了数组 $p$,那么左指针就是 $i - p_i$,右指针就是 $i+p_i$。
但是在原来的算法当中,左右指针都是从 $i-1$ 和 $i+1$ 开始的,即都是从 $p_i=1$开始枚举的,现在有没有办法不从1开始枚举呢。
- 之前的中心枚举算法,我们从$1$ 到 $n$ 的枚举中心。向两侧扩张然后记录最长回文串,现在我们在记录最长回文串的过程中,将枚举过的最长回文串的中心记为 $id$ 和右边界记录为 $mx$。
由于回文串具有对称性,因此 $p$ 的初值也应该具有某种对称性,我们分类讨论如下,假设当前枚举中心为 $id$,需要求的位置 $p_i$
- 若 $mx > i$
说明需要求的位置在枚举过的最长回文串内部,那么根据回文串的对称性,找到关于 $id$ 对称的位置$2*id-1$ ,那么 $p_i$ 的值就可以利用 $p_{2*id-1}$得到。
同时,就算是对称,只保证以 $i$ 为中心,右边界最大为 $mx$ 的范围内是回文串,因此结果有边界还需要在 $mx$ 范围内,因此得到$$p_i = min(p_{2*id-1}, mx - i)$$
这个时候再暴力的向两边扩张得到最终的 $p_i$ 值
- 若 $mx <= i$
说明当前的位置并没有被枚举过的最长回文串包围,因此没法利用对称性,这个时候$$p_i=1$$
再暴力的向两边扩张得到最终的 $p_i$ 值
- 若 $mx > i$
- 在所有的$p_i$ 值得以后,求解答案的方法和中心对称一样
时间复杂度 $O(n)$
总结
Manacher我个人理解是对中心枚举算法的一种优化,利用回文串的对称性质,快速得到 $p_i$ 的较好的初始值,而不是每一次都从1开始枚举。
代码
int manacher(string s){
string temp = "$#";
for(int i=0; i<s.length(); i++){
temp += s[i];
temp += '#';
}
vector<int> p(temp.length(),0);
int mx = 0,id = 0,len = 0,cter = 0;
for(int i=1; i<temp.length(); i++){
p[i] = mx > i ? min(p[id*2-i],mx-i) : 1;
while(temp[i+p[i]] == temp[i-p[i]]) ++p[i];
if (mx < i + p[i]){
mx = i + p[i];
id = i;
}
if (len < p[i]){
len = p[i];
cter = i;
}
}
return len-1;
//return s.substr((center-len)/2,len-1);
}
回文树
对于上述的问题当然不止一种解决方法,我们将介绍另一个处理字符串的强有力的工具,回文树。字符串 $caacbca$ 的回文树形态如下,接下来我们会一步步拆解出如何构建回文树。
简介
- 回文树包含了给定字符串中所有的本质不同回文字串
- 回文树节点个数不超过 $O(n)$
- 回文树有两个根节点,两棵树,一棵树描述奇数长度回文串一个描述偶数长度回文串
定义
节点
回文树上的每一个节点都代表一个回文串
边
边均为有向边$(u,v,ch)$ 表示在节点 $u$ 所代表的回文串两边添加字符 $a$ ,得到 $v$ 所代表的回文串
$fail$
$fail_i$ 表示第 $i$ 个节点所代表得回文串的最长后缀回文串
回文树可以拆分成上述三个部分,那么现在开始一步步构建回文树
构建
回文树的构建是在线一个字符一个字符插入的。假设已经对$S$的某个前缀 $P$ 建好了回文树,前缀 $P$ 的最长后缀回文串记录为 $Q$,$Q$的前一个字符为 $y$,现在需要新插入字符 $x$。
我们考虑新加入 $x$ 会新增加多少回文串。现在的回文树已经包含了所有前缀的回文串,在添加 $x$ 字符后新的回文串必定以 $x$ 字符为结尾。
- 若 $x=y$
那么 $yQx=xQx$,并且 $Q$ 也是一个回文串,那么 $yQx$ 就是新产 生的回文串
- 若 $x \neq y$
这个时候,$yQx$ 不是回文串了,虽然 $y+Q$ 没办法让 $x$ 构成回文串了,但是$Q$是后缀上最长的回文串,后缀上还有第二长第三长的回文串有可能可以让 $x$ 构成回文串。
这个时候发现, $Q$的$fail$指向的是$Q$的最长后缀回文,也就是 $P$ 的第二长后缀回文,那么令 $Q' = fail_Q,y'=fail_Q$的前一个字符这时候,又重新回到了上述判断,$y'$ 是否等于 $x$,重复以上步骤,直到找到某个 $y'=x$为止。
通过上述的步骤我们找到了新增加的回文串 $xQx$,这个时候还差最后一步,就是找到新增加回文串对应的$fail$,因为 $xQx$ 的最长后缀回文也是以 $x$ 为结尾,因此不断的从 $Q$继续跳$fail$,直到找到一个位置 $xQ'x$,这个节点就是$xQx$节点的$fail$指向的节点。
按照上述步骤,将每一个字符都插入,就得到了$S$的回文树
应用
例题2
我们在建立回文树的过程中会记录每个节点代表的回文串长度,那么我们遍历所有节点取最大值即可,回文树总节点个数 $O(n)$,时间复杂度 $O(n)$
例题3
现在给定了一个字符串长度$10^6$,包含0-9数字,求所有回文字串对应的数字对 1e9 + 7取模的和。
这种题目Manacher也可以做,但是非常的麻烦,回文树就非常简单处理,在新添加节点的时候,记录新节点的价值为 $v_i$,父亲节点价值为 $v_{fa}$,那么就有
$$v_i = x*10^{len(v_{fa})+1}+v_{fa}*10 + x$$
求和即可,这些在Manacher内部是非常难以实现的。
代码
struct Palindrome_Aho{
int len[maxn],nex[maxn][26];
int fail[maxn],s[maxn];
int n,totNode,last,cur;
LL ans,cnt[maxn];
int newnode(int p){
for(int i=0; i<26; i++) nex[totNode][i] = 0;
cnt[totNode] = 0; len[totNode] = p;
return totNode++;
}
void init(){
totNode = last = n = 0;
newnode(0); newnode(-1);
s[0] = -1;
fail[0] = 1;
}
int Fail(int x){
while(s[n-len[x]-1] != s[n]) x = fail[x];
return x;
}
void insert(int ch){
s[++n] = ch;
cur = Fail(last);
if (!nex[cur][ch]){
int now = newnode(len[cur]+2);
fail[now] = nex[Fail(fail[cur])][ch];
nex[cur][ch] = now;
}
last = nex[cur][ch];
cnt[last]++;
}
void count(){
for(int i=totNode-1; i>=0; i--){
}
}
}pam;
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。