引入
在字符串问题处理中,常常会遇见处理回文串的问题。那么首先就要介绍一下什么是回文串——回文串就是对于一个字符串,自左向右和自右向左所得的字符串是相同的。例如 a b b a \;abba\; abba, x y x \;xyx\; xyx, a \;a\; a等都是回文串,而 a b c d \;abcd\; abcd, a b b c \;abbc\; abbc, a b c \;abc\; abc等则不是回文串。
实现
那么如何来实现求解一个字符串中所有回文串问题呢?先考虑最朴素的实现方法,即枚举字符串的每一个位置,然后不断向两边扩展,直到二者不同,则得到了以该位置为中心的回文串的长度,显然每个位置的回文串长度至少为 1 \;1\; 1。特别地,对于回文串而言,还有特别考虑长度的奇偶问题,因为对于奇长度的回文串其中心位置是一个字符,而偶长度的回文串则不是,但无论如何该算法的复杂度都达到了 O ( n 2 ) \;O(n^2)\; O(n2),往往是不足以接受的。
那么
M
a
n
a
c
h
e
r
Manacher
Manacher算法是如何将复杂度降到
O
(
n
)
\;O(n)\;
O(n)的呢,其设计思想类似于Z函数(戳我一键直达),考虑到回文串的对称性,观察下图:
当我们需要求解以
i
i
\;i\;i
ii为中心的回文串长度时,我们已经得知了在
i
\;i\;
i之前的一个位置
m
i
d
\;mid\;
mid,以它为中心的回文串的包含了
i
\;i\;
i位置的字符,那么根据回文串的对称性,我们可以得到对称点
j
\;j\;
j的信息,而该位置的回文串长度是已经求解过的,那么就在以
m
i
d
\;mid\;
mid为中心的回文半径中(即图中蓝色部分),我们可以得到
i
\;i\;
i的回文半径的长度和
j
\;j\;
j的回文半径存在关联,这种关联可以极大地减少回溯次数,从而降低了复杂度。其分为下述两种情况:
1.以
j
\;j\;
j为中心的回文半径未超出以
m
i
d
\;mid\;
mid为中心的回文半径
对于这种情况,我们就可以
O
(
1
)
\;O(1)\;
O(1)地得到以
i
\;i\;
i为中心的回文半径,利用反证法,如果以
i
\;i\;
i为中心的回文半径还要更长,因为此时这两个位置的回文边界都处于
m
i
d
\;mid\;
mid的回文串之中,所以二者是严格对称的,即回文半径相同。
2.以
j
\;j\;
j为中心的回文半径未超出以
m
i
d
\;mid\;
mid为中心的回文半径
对于这种情况,因为已经超出了以
m
i
d
\;mid\;
mid为中心的回文串范围,所以利用已有的对称性我们只能保证在
i
~
r
\;i~r\;
i~r这段区间的长度至少是以
i
\;i\;
i为中心的回文半径,而对于
r
\;r\;
r之后的位置,则需要枚举去对比。
那么问题来了,上文提到过,回文串的长度是分奇偶的,刚刚的论述过程似乎只适用于奇长度的回文串,那偶长度该如何处理呢?其实方法类似,只需要修改一下临界位置的条件即可,这里不给出实现代码给出了也没人用,毕竟可以二合一谁还那么麻烦去分类讨论 。为了避免繁琐的分类导致出现差错,在这里有一个实现技巧,即在字符串中每个字符之间插入一个特殊字符,例如将
a
b
b
a
b
b
a
\;abbabba\;
abbabba改写为
#
a
#
b
#
b
#
a
#
b
#
b
#
a
#
\;\#a\#b\#b\#a\#b\#b\#a\#\;
#a#b#b#a#b#b#a#,这样不论是奇长度还是偶长度就可以统一处理了。另外需要注意的是,因为引入了特殊字符,导致字符串长度会增加
2
\;2\;
2倍,实现是要注意空间大小以防溢出,另外就是得到每个位置的回文半径因为有特殊字符的影响,所以实际长度=所求长度/2。
实现代码如下:
void read()
{
char ch = getchar();
len = 1;
while (ch < 'a' || ch>'z')
{
ch = getchar();
}
while (ch >= 'a' && ch <= 'z')
{
s[len++] = '#';
s[len++] = ch;
ch = getchar();
}
s[len] = '#';
}
void manacher()
{
for (int i = 1, r = 0, mid = 0; i <= len; i++)
{
if (i <= r)
{
p[i] = min(p[(mid << 1) - i], r - i + 1);
}
while (s[i - p[i]] == s[i + p[i]])
{
p[i]++;
}
if (p[i] + i > r)
{
r = p[i] + i - 1;
mid = i;
}
}
}
例题解析:
C
o
d
e
f
o
r
c
e
s
17
E
—
—
P
a
l
i
s
e
c
t
i
o
n
Codeforces\ 17E——Palisection
Codeforces 17E——Palisection
题目大意:找出一个字符串中所有相交的回文串的个数。
题意分析:考虑到求解相交回文串并不容易,所以逆向考虑,即总共的回文串数量减去互不相交的回文串的个数即为所求答案,另外需要注意的是这道题也运用到了差分的知识。
Solution:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 4e6 + 16;
const ll mod = 51123987ll;
char s[maxn];
int n, len, p[maxn];
int l[maxn], r[maxn];
ll ans;
inline void read()
{
char ch = getchar();
s[0] = '$';
while (ch < 'a' || ch > 'z')
{
ch = getchar();
}
while (ch >= 'a' && ch <= 'z')
{
s[++len] = '#';
s[++len] = ch;
ch = getchar();
}
s[++len] = '#';
}
inline void manacher()
{
for (int i = 1, r = 0, mid = 0; i <= len; i++)
{
if (i < r)
{
p[i] = min(p[(mid << 1) - i], r - i + 1);
}
else
{
p[i] = 1;
}
while (s[i - p[i]] == s[i + p[i]])
{
p[i]++;
}
if (p[i] + i > r)
{
r = p[i] + i - 1;
mid = i;
}
}
}
inline void solve()
{
for (int i = 1; i <= len; i++)
{
//利用差分处理,l[i]表示以i为起点的回文串,r[i]表示以i为终点的回文串
l[i - p[i] + 1]++;
l[i + 1]--;
r[i]++;
r[i + p[i]]--;
ans = (ans + ((ll)p[i] / 2)) % mod; //统计所有回文串的数量
}
ans = ans * (ans - 1) / 2 % mod; //假设回文串之间都能相交
ll s = 0;
for (int i = 1; i <= len; i++)
{
//利用差分求出原始序列
l[i] += l[i - 1];
r[i] += r[i - 1];
if (!(i & 1)) //偶数位置即原字符串中的字符
{
//s表示之前结束的回文串,与当前的l[i]相乘就是不相交的个数
ans = (ans - s *(ll) l[i] % mod) % mod;
s = (s +(ll) r[i]) % mod;
}
}
cout << ((ans + mod) % mod) << endl;
}
int main()
{
cin >> n;
read();
manacher();
solve();
return 0;
}
完结撒花!!!