题目
思路
可以看看 y y b \tt yyb yyb 大佬的博客。如果你直接觉得 “ 没问题!简单!” 那就不用看这篇博客了 😉
第一步,令
S
′
=
S
1
S
n
S
2
S
n
−
1
S
3
S
n
−
2
…
S
n
2
S
n
2
+
1
S'=S_1S_nS_2S_{n-1}S_3S_{n-2}\dots S_{\frac{n}{2}}S_{\frac{n}{2}+1}
S′=S1SnS2Sn−1S3Sn−2…S2nS2n+1 ,那么问题转化为,将
S
′
S'
S′ 划分为若干个偶数长度的回文串。这个构造请自己手推。
用 f ( x ) f(x) f(x) 表示长度为 x x x 的前缀的划分方案数。建立回文自动机之后,不难想到 O ( n 2 ) \mathcal O(n^2) O(n2) 的暴力跳 f a i l fail fail 的转移。为了让 “ 偶数长度 ” 这个奇怪的限制滚蛋,直接令 f ( 2 x + 1 ) = 0 f(2x+1)=0 f(2x+1)=0 就可以无差别转移了。
第二步,由于转移只是单纯的 ∑ f \sum f ∑f ,可以考虑用 g g g 存储一段 d i f ( x ) = l e n ( x ) − l e n ( f a i l ) dif(x)=len(x)-len(fail) dif(x)=len(x)−len(fail) 相等的点。
然而,这一步有问题了!那就是, g g g 到底代表哪些 f f f 的和?因为字符串的右端点是在改变的!
我的理解是,事实上 g ( x ) = ∑ k f [ ω − 1 + k d i f ( x ) ] g(x)=\sum_{k}f[\omega-1+k\;dif(x)] g(x)=∑kf[ω−1+kdif(x)] ,其中 ω \omega ω 是该字符串 最后一次出现 的左端点位置。而 k k k 的范围是什么呢?当然是 “ d i f dif dif 等值链 ” 的长度。
此时重新考虑这个问题。加入一个字符,会影响哪些 g g g 的值呢?也就是 ω \omega ω 什么时候会变?显然是后缀。那么我们只要把新点 f a i l fail fail 链上的 g g g 更新了即可!
好,我们继续。怎么转移
g
g
g 呢?考虑
f
a
i
l
(
x
)
fail(x)
fail(x) 的
g
g
g 是哪些
f
f
f 。众所周知,对于一个回文自动机,新产生的后缀串
S
t
r
(
f
a
i
l
(
x
)
)
Str(fail(x))
Str(fail(x)) ,如果长度不小于
l
e
n
(
x
)
2
\frac{len(x)}{2}
2len(x) ,其最后一次出现恰好是将其用
S
t
r
(
x
)
Str(x)
Str(x) 翻转后的结果!
这个东西挺好证。首先,用 S t r ( x ) Str(x) Str(x) 将其翻转后结果必然是一次出现。其次,如果它有更靠后的出现,那么这两个串取并也是一个回文串,与 f a i l ( x ) fail(x) fail(x) 是最长的回文后缀冲突。
要不咱还是画个图?虽然是用代码块的像素风。
Str(x): ------xxxxxxxxxx
Str(fail): ---------xxxxxxx
REVERSE, Str(fail): ------xxxxxxx---
if Str(fail): -------xxxxxxx--
then Str(?): -------xxxxxxxxx
不难验证,上图中的
S
t
r
(
?
)
Str(?)
Str(?) 是一个回文串,它是由别的
S
t
r
(
f
a
i
l
)
Str(fail)
Str(fail)(我们假设的一个,在 if
语句中的)和后缀
S
t
r
(
f
a
i
l
)
Str(fail)
Str(fail) 拼接而成的。如果它真的存在,它肯定会成为
f
a
i
l
fail
fail ,因为它更长。
既然这个结论成立,那么我们的更新就可以变的骚一点。先记 g ( x ) g(x) g(x) 恰好管不着的那个点为 t o p ( x ) top(x) top(x)(确实很像链剖分)。如果 t o p ( x ) = f a i l ( x ) top(x)=fail(x) top(x)=fail(x) ,直接更新即可(因为此时 g g g 只能管自己的值)。否则, d i f ( x ) < l e n ( x ) 2 dif(x)<\frac{len(x)}{2} dif(x)<2len(x) ,毕竟 d i f [ f a i l ( x ) ] = d i f ( x ) dif[fail(x)]=dif(x) dif[fail(x)]=dif(x) 嘛,跳两下父节点的话,长度要变小 2 d i f ( x ) 2\;dif(x) 2dif(x) 的。
于是, l e n [ f a i l ( x ) ] > l e n ( x ) 2 len[fail(x)]>\frac{len(x)}{2} len[fail(x)]>2len(x) ,恰好满足我们上面的结论触发条件。所以 f a i l ( x ) fail(x) fail(x) 和 x x x 都满足 ω = n − l e n ( x ) + 1 \omega=n-len(x)+1 ω=n−len(x)+1 。那么 g [ f a i l ( x ) ] g[fail(x)] g[fail(x)] 和 g ( x ) g(x) g(x) 的唯一差别就只在于定义式中 k k k 的范围。显然 x x x 对应的 k k k 的范围比 f a i l ( x ) fail(x) fail(x) 的大 1 1 1 。直接加上这个差异点 f [ n − l e n ( t o p ) − d i f ] f[n-len(top)-dif] f[n−len(top)−dif] 即可。
用行中公式写一下: g ( x ) = g [ f a i l ( x ) ] + f [ n − l e n ( t o p ) − d i f ] g(x)=g[fail(x)]+f[n-len(top)-dif] g(x)=g[fail(x)]+f[n−len(top)−dif]
如果你没有看懂上面这段话!请看 l l s w \tt llsw llsw 题解中的最后一张图片。
E x t r a E x p l a n a t i o n Extra\;Explanation ExtraExplanation:为啥差异点是那个玩意儿?因为 g ( x ) g(x) g(x) 恰好管不着的位置是 n − l e n ( t o p ) n-len(top) n−len(top) 嘛,往回退一步就是管的到的。
所以,只要
f
a
i
l
(
x
)
fail(x)
fail(x) 信息正确,就可以求出
g
(
x
)
g(x)
g(x) 。这么显然应该不用加粗才对。
可是这样仍然是 O ( n 2 ) \mathcal O(n^2) O(n2) 的,因为整条链的 g g g 都要被修改……
给出骚操作:每次更新,只更新 “ d i f dif dif 等值链 ” 的交界处 和当前点(目前的整个串的最长回文后缀)。这样就足以保证正确性。
怎么想到的?不知道。但是正确性容易证明。考虑没有被及时更新的点,其本质是 d i f ( x ) = d i f ( s o n x ) dif(x)=dif(son_x) dif(x)=dif(sonx)(虽然并没有 s o n son son 这个数组,意会就好),否则就会成为链的交界处。
假设我们正在更新 g ( y ) g(y) g(y) 。考虑一下, g [ f a i l ( y ) ] g[fail(y)] g[fail(y)] 是不是在我们想要的 n − d i f ( y ) n-dif(y) n−dif(y) 处更新了呢?不妨用反证法,假设 当初 它没有被更新,因为它被子节点 x x x 跳了过去,满足 f a i l ( x ) = f a i l ( y ) fail(x)=fail(y) fail(x)=fail(y) 且 d i f ( x ) = d i f [ f a i l ( x ) ] dif(x)=dif[fail(x)] dif(x)=dif[fail(x)] 。
首先你要意识到, d i f dif dif 就是周期,因为 f a i l fail fail 就是 b o r d e r \rm border border 。有了这一点,你就会意识到 d i f dif dif 在 f a i l fail fail 链上是单减的(不然周期不会变)。由于我们要更新 y y y ,所以 d i f ( s o n y ) > d i f ( y ) dif(son_y)>dif(y) dif(sony)>dif(y) 。
并且,因为 d i f dif dif 是周期,所以 S t r ( x ) Str(x) Str(x) 一定是 S t r [ f a i l ( x ) ] Str[fail(x)] Str[fail(x)] 前面加上一个周期(一定是完整的周期)得到的结果。而我们知道, S t r [ f a i l ( x ) ] Str[fail(x)] Str[fail(x)] 当初的右端点为 n − d i f ( y ) n-dif(y) n−dif(y) ,恰好是 S t r ( y ) Str(y) Str(y) 去掉从右边开始数的一个周期(尽管可能不是所谓的完整周期,但是确实为一个周期的长度),所以 S t r ( x ) = S t r ( y ) \color{black}Str(x)=Str(y) Str(x)=Str(y) 。
还是 用代码块风格 画一张图。要想到这一点:
f
a
i
l
fail
fail 只跟这个节点本身有关,所以无论右端点怎么变,
f
a
i
l
fail
fail 不变,进而
d
i
f
dif
dif 也不变。因为
f
a
i
l
(
y
)
fail(y)
fail(y) 的
d
i
f
dif
dif 不变,所以导致它被跳过的
x
x
x 也就固定了。
.............. : son(y) (whatever string)
abababababa : y (dif vary, so it's processed)
ababababa : fail(y)
abababa : fail(fail(y)) (just to show dif = 2)
ababababa : expected fail(y)
abababababa : x, who caused skip on fail(y)
ababababababa : x∪y, who should be fail(son(y))
显然 S t r ( x ) Str(x) Str(x) 和 S t r ( y ) Str(y) Str(y) 相交,所以二者的并也是回文串。不难发现这是从 n − d i f ( y ) − l e n ( y ) + 1 n-dif(y)-len(y)+1 n−dif(y)−len(y)+1 到 n n n 的。然鹅, d i f ( s o n y ) > d i f ( y ) ⇒ l e n ( s o n y ) > l e n ( y ) + d i f ( y ) dif(son_y)>dif(y)\Rightarrow len(son_y)>len(y)+dif(y) dif(sony)>dif(y)⇒len(sony)>len(y)+dif(y) ,所以这个新回文串仍然包含在 s o n y son_y sony 以内。也就是说!在 S t r ( s o n y ) Str(son_y) Str(sony) 内!有一个比 f a i l fail fail 更长的回文后缀!矛盾!
当然,你会说,如果 y y y 是当前点而寻求更新呢?不一定满足 d i f ( s o n y ) > d i f ( y ) dif(son_y)>dif(y) dif(sony)>dif(y) 了吧(因为根本没有子节点)?然而这种情况 S t r ( x ) = S t r ( y ) Str(x)=Str(y) Str(x)=Str(y) 是仍然成立的。怎么可能新加入的点代表一个已经存在的字符串呢?
总结:虽然我们的更新不完全,但它足以应付我们需要用到的 g g g 了。
时间复杂度呢?由于 d i f dif dif 是 b o r d e r \rm border border ,所以一定要 l e n ≤ d i f len\le dif len≤dif 时才能切换一个 d i f dif dif 。这个分析就跟辗转相除一样了:如果 d i f dif dif 不超过 l e n 2 \frac{len}{2} 2len ,那么余数自然不能超过 d i f dif dif ;而 d i f dif dif 超过一半长度时,一步到位。最多跳 O ( log n ) \mathcal O(\log n) O(logn) 次。
代码
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
const int Mod = 1e9+7;
const int MaxN = 1000005;
const int CharSiz = 26;
int f[MaxN], g[MaxN];
namespace PAM{
int ch[MaxN][CharSiz];
int fail[MaxN], cntNode = 1;
char item[MaxN]; int len[MaxN];
int n = 0, lst = 1, top[MaxN];
void init(){
++ cntNode; // include 0
memset(ch,0,cntNode*CharSiz<<2);
memset(fail,0,cntNode<<2);
n = 0, cntNode = lst = 1;
fail[0] = fail[1] = 1;
len[1] = item[0] = -1;
}
void add(char c){
item[++ n] = c; int x = lst;
while(item[n-len[x]-1] != item[n])
x = fail[x]; // find it
int now = ch[x][c];
if(now == 0){
now = ++ cntNode; // new node
int &p = fail[now] = fail[x];
while(item[n-len[p]-1] != item[n])
p = fail[p]; // find it
p = ch[p][c]; // add char c
ch[x][c] = now; // last step
len[now] = len[x]+2;
if((len[fail[now]]<<1) ==
len[now]+len[fail[fail[now]]])
top[now] = top[fail[now]];
else top[now] = fail[now];
}
lst = now; // update lst
}
void solve(){
int p = lst;
for(; p&&p!=1; p=top[p]){
int dif = len[p]-len[fail[p]];
g[p] = f[n-len[top[p]]-dif];
if(fail[p] != top[p])
g[p] = (g[p]+g[fail[p]])%Mod;
if(!(n&1)) f[n] = (f[n]+g[p])%Mod;
}
}
}
char xez[MaxN], tmp[MaxN];
int main(){
scanf("%s",tmp);
int zxy = strlen(tmp);
for(int i=0; (i<<1)<zxy; ++i)
xez[i<<1] = tmp[i];
for(int i=0; (i<<1|1)<zxy; ++i)
xez[i<<1|1] = tmp[zxy-1-i];
PAM::init(), f[0] = 1;
for(int i=0; i<zxy; ++i){
PAM::add(xez[i]-'a');
PAM::solve();
}
printf("%d\n",f[PAM::n]);
return 0;
}
双倍经验
这道题做法没有任何区别。同样的构造方式。同样的优化方式。