对回文串border理论的初探。本文需要回文自动机前缀知识。
        以下部分文为自证,非原论文证明。

        为了方便叙述,我们用$S$表示一个字符串,用$|S|$表示其长度,$S[i],1\leq i \leq |S|$表示其第$i$个字符。

border

        对于一个字符串$S$,我们记$pre(S,i)$为其长度为$i$的前缀,即$S[1…i]$,记$suf(S,i)$为长度为$i$的后缀,即$S[|S|-i+1…|S|]$。如果$1\leq i< |S|,pre(S,i)=suf(S,i)$,我们称$pre(S,i)$为字符串$S$的border。
        如果对于$0<t\leq |S|$,若$\forall i,1\leq i\leq|S|-t$,有$S[i]=S[i+t]$,则称$t$为$S$的周期。
        关于border与周期,有如下的定理:

  • 【定理1】$T$是$S$的border的充要条件是$|S|-|T|$为$S$的周期。

证明:
        充分性:若$|S|-|T|$是$S$的周期,则对于$\forall i,1\leq i\leq |T|$,有$S[i]=S[i+|S|-|T|]$,这显然说明$pre(S,|T|)=suf(S,|T|)$,即$T$是$S$的border。
        必要性:若$pre(S,|T|)=suf(S,|T|)$,则有$\forall i,1\leq i\leq |T|$有$S[i]=S[i+|S|-|T|]$,从而得到$|S|-|T|$为$S$的周期。

回文border的相关理论

        了解了border的定义后,我们会证明一个重要的定理:

  • 【定理2】对于字符串$S$,将其所有回文后缀按长度排序后,可以划分为$log|S|$段等差数列。

        在证明这个理论之前,先引入四个引理:

  • 【引理2.1】对于回文串$S$,$T$是$S$的后缀,则$T$是$S$的border的充要条件是$T$是回文串。

证明:
        若$T$是回文串,则$\forall |S|-|T|+1\leq i\leq |S|,S[i]=S[2|S|-|T|+1-i]$。又根据$S$是回文串,可以得到$S[i]=S[2|S|-|T|+1-i]=S[|S|+1-i]=S[|T|-|S|+i]$,这就证明了$pre(S,|T|)=suf(S,|T|)$,从而$T$是$S$的border。同理可证必要性。

  • 【引理2.2】对于字符串$S$及其border$T$,若$|S|\leq 2|T|$,则$S$为回文串的充要条件是$T$为回文串。

证明:
        $\forall i,1\leq i\leq |T|$,根据$T$为border且为回文串有:

        这就证明了$1\leq i\leq |T|$时,$S$符合回文串性质。由于$|S|\leq 2|T|$,可知$|T|\geq \frac {|S|}{2}$,从而$S$就是回文串。
        必要性同理。

  • 【引理2.3】对于回文串$S$,$|S|-|T|$为$S$的最小周期的充要条件是$T$是$S$的最长回文真后缀。

证明:
        首先根据定理1可知$|S|-|T|$是最小周期等价于$T$是$S$的最长border。
        由于$T$是回文后缀,则根据引理2.1,$T$是$S$的border。并且由于$T$是最长回文真后缀,可知$T$为最长border。必要性根据引理2.1可以反向推知。

  • 【引理2.4】对于回文串$X$,其最长回文真后缀为$Y$,$Y$的最长回文真后缀为为$Z$,设字符串$U,V$满足$X=UY,Y=VZ$,则有下面的性质成立:
  1. $|U|\geq |V|$。
  2. 若$|U|>|V|$,则$|U|>|Z|$。
  3. 若$|U|=|V|$,则$U=V$。

证明:
        对于性质一,我们采用反证法,我们用$\overline S$来表示$S$的翻转。假设$|V|>|U|$。由于$Y$是回文串,必有$VZ=Z\overline V$,并且因为$X$为回文串,可知$UVZ=UZ\overline V=VZ\overline U$,由此可知$U$为$V$的前缀,不妨令$V=US$,这里$|S|>0$。那么由于$X$为回文串,可得$UVZ=UUSZ=Z\overline S \overline U\overline U$,由$Y$是回文串可得$VZ=USZ=Z\overline S\overline U$。于是可得$UZ\overline S\overline U=Z\overline S\overline U\overline U$,即$UZ\overline S=Z\overline S\overline U=USZ$,从而$Z\overline S=SZ$,这样我们就得到了比$Z$更长的回文后缀$SZ$,这与$Z$为$Y$的最长回文后缀矛盾,因此必有$|U|\geq |V|$。
        性质二同样采用反证法。由性质一中证明的思路,我们可以发现$|U|>|V|$时,$V$为$U$的前缀,不妨设$U=VP$,然后假设$|U|\leq |Z|$。根据$UZ\overline V=Z\overline V\overline U$,可知$U$为$Z$的前缀,不妨设$Z=US,|S|\geq 0$,这样经过代入,可以得到$UUS\overline V=US\overline V\overline U=US\overline V\overline P\overline V$,从而有$US=VPS=S\overline V\overline P=Z$。由于$Z$是回文串,故$S\overline V\overline P=PV\overline S=VPS$,所以$PV=VP,S=\overline S$。于是:

        由此我们得到了$PY$这个比$Y$更长的回文后缀,这与$Y$为最长回文后缀矛盾。从而性质二成立。
        性质三从上面证明中可以发现显然成立。

        我们现在来证明定理2。在这里我们仍用引理2.4的串$X,Y,Z$来说明。
        注意到等差的条件就是$|U|=|V|$,根据引理2.4我们只需要证明$|U|>|V|$的次数在$log|S|$级别即可。由于$|U|>|V|$,有$|U|>|Z|$,于是$2|U|>|V|+|Z|=|Y|$,即$|Y| < 2|U|=2(|X|-|Y|)$,从而有$|Y|<\frac {2|X|}{3}$,那么$|U|>|V|$的出现次数必然在$log_{1.5}|S|$级别,证毕。

border理论在PAM上的应用

        border理论究竟有什么用,我们至今还没有提及。事实上border理论是一个将一些在PAM上的问题的复杂度降为$O(nlogn)$的理论基础。
        考虑最小回文划分问题:给一个字符串$S$,求一个划分$S_1+S_2+\cdots+S_k=S$,使得$S_i$为回文串,最小化$k$。对于这个问题,我们可以考虑$DP$,用$f(x)$表示前$x$位的答案,然后状态转移方程为:

        建出PAM后,我们可以在某一个节点的回文后缀上进行转移,这样就可以在$O(n^2)$的复杂度下解决这个问题。利用border理论的定理2,这个复杂度可以降为$O(nlogn)$。
        利用等差数列段数在$log|S|$级别这个性质,我们可以维护一段等差数列的信息,从而只需要进行$log|S|$次转移,从而降低复杂度。
        在PAM上,每一个节点需要多维护两个信息$diff[x]$以及$slink[x]$,其中$diff[x]$表示其与$fail[x]$即最长回文真后缀的长度差,即$len[x]-len[fail[x]]$。而$slink[x]$表示向上的第一个节点$u$使得$diff[x]\not=diff[u]$,这样$x$到$slink[x]$这些回文串的长度差都是相同的。我们将$DP$的信息放到每一段等差回文串中最长的那一个串节点上。
        我们给每一个节点再维护一个$g[x]$数组,当枚举到第$i$个字符时,定义如下:

        注意$slink[x]$算到下一个等差数列中。
        每一次枚举$i$,都需要更新$g$数组和$f$数组,考虑如何更新。
        假设当前枚举到了$i$,对应的节点是$x$,可以发现$fail[x]$对应的串上一次出现的位置必然是$i-diff[x]$,那么根据$g$的定义,$g[fail[x]]$为:

        考虑到$g[x]$应该为:

        我们假设$g[fail[x]]$维护的序列按长度降序排序是$fail[x]\cdots p$,这里显然$fail[p]=slink[fail[x]]$。假设$len[x]-len[p]=ndiff[x]$,那么$g[fail[x]]$可以表示为:

        注意到此时$g[x]$可以表示为:

        两者比较可以发现:

        $len[p]$显然等于$len[slink[x]]+diff[x]$,那么就有:

        这样我们就得到了利用已有的$g$数组更新的方法,于是向上跳$slink[x]$,就可以完成更新。
        这里给出利用border理论完成的最小回文划分问题代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <bits/stdc++.h>

#define N 500005
using namespace std;
struct Node
{
int len, fa, ch[26];
} nd[N << 1];
int ct = 1, last, diff[N], slink[N], dp[N], g[N];
char op[N];

inline int newNode(int x)
{
int p = ++ct;
nd[p].len = x;
return p;
}

inline void add(int c, int pos)
{
int u = last;
while (op[pos - nd[u].len - 1] != op[pos]) u = nd[u].fa;
if (nd[u].ch[c] == 0) {
int s = newNode(nd[u].len + 2), f = nd[u].fa;
while (op[pos - nd[f].len - 1] != op[pos]) f = nd[f].fa;
nd[s].fa = nd[f].ch[c], nd[u].ch[c] = s;
}
last = nd[u].ch[c];
diff[last] = nd[last].len - nd[nd[last].fa].len;
if (diff[last] == diff[nd[last].fa]) slink[last] = slink[nd[last].fa];
else slink[last] = nd[last].fa;
}

int main()
{
nd[0].len = 0, nd[0].fa = 1, nd[1].len = -1, nd[1].fa = 0;
scanf("%s", op + 1);
for (int i = 1; op[i]; i++) {
add(op[i] - 'a', i), dp[i] = 0x7fffffff;
for (int j = last; j > 1; j = slink[j]) {
g[j] = dp[i - nd[slink[j]].len - diff[j]];
if (diff[j] == diff[nd[j].fa]) g[j] = min(g[j], g[nd[j].fa]);
dp[i] = min(dp[i], g[j] + 1);
}
}
printf("%d", dp[strlen(op + 1)]);
return 0;
}

例题

        涉及回文border理论的字符串问题大多比较难,不过它们的重点都是要善于利用等差数列的性质。

[GYM102864I]shenyunhan Loves Palindrome

        可以发现一段子串$[l,r]$合法的充要条件是:

  • $[l,r]$是一个回文串
  • $[l,r]$可以有两个非空回文串拼接组成

        第一个很容易判,难的是第二个。
        我们构建两个PAM,其中一个以正序顺序建立,另一个将串反串后建立。在回文树上倍增可以找到两段树链,分别为子串$[l,r]$的回文前缀以及回文后缀。现在问题转化为找两个节点,使得长度之和为$r-l+1$。
        对于这个问题,朴素的算法最好也是$O(n)$的。但是使用回文border引理可以优化到$O(log^3n)$。
        由于等差数列是$O(logn)$的,我们可以暴力地枚举两个树链上的等差数列,复杂度为$O(log^2n)$。然后用扩展欧几里得算法判定解的存在性即可,总体复杂度即为$O(nlog^3n)$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#include <bits/stdc++.h>

#define N 500005
using namespace std;
char op[N];
int n, q;
struct Node
{
int len, fa, ch[26];
};
struct Edge
{
int next, to;
};
struct PAM
{
Node nd[N];
Edge edge[N << 1];
int ct = 1, last, diff[N], slink[N], sdep[N], suf[N], head[N], cnt = 1, gr[N][20];

inline void addEdge(int x, int y)
{
edge[cnt].next = head[x], edge[cnt].to = y, head[x] = cnt++;
}

inline int newNode(int x)
{
int p = ++ct;
nd[p].len = x;
return p;
}

inline void add(int c, int pos)
{
int u = last;
while (op[pos - nd[u].len - 1] != op[pos]) u = nd[u].fa;
if (nd[u].ch[c] == 0) {
int s = newNode(nd[u].len + 2), f = nd[u].fa;
while (op[pos - nd[f].len - 1] != op[pos]) f = nd[f].fa;
nd[s].fa = nd[f].ch[c], nd[u].ch[c] = s;
}
last = nd[u].ch[c];
diff[last] = nd[last].len - nd[nd[last].fa].len;
if (diff[last] == diff[nd[last].fa])
slink[last] = slink[nd[last].fa], sdep[last] = sdep[nd[last].fa] + 1;
else
slink[last] = nd[last].fa, sdep[last] = 1;
}

void DFS(int x, int fa)
{
for (int i = head[x]; i; i = edge[i].next) {
if (edge[i].to == fa) continue;
gr[edge[i].to][0] = x;
for (int j = 1; j < 20; j++) gr[edge[i].to][j] = gr[gr[edge[i].to][j - 1]][j - 1];
DFS(edge[i].to, x);
}
}
void init()
{
nd[0].len = 0, nd[0].fa = 1, nd[1].len = -1, nd[1].fa = 0;
}
void insert()
{
for (int i = 1; i <= n; i++) add(op[i] - 'a', i), suf[i] = last;
for (int i = 2; i <= ct; i++) addEdge(nd[i].fa, i);
DFS(0, -1);
}
} pam1, pam2;
int gcd(int x, int y)
{
return y == 0 ? x : gcd(y, x % y);
}
int exGcd(int a, int b, int& x, int& y)
{
if (b == 0) {
x = 1, y = 0;
return a;
}
int t = exGcd(b, a % b, y, x);
y -= a / b * x;
return t;
}
int main()
{
scanf("%d%d%s", &n, &q, op + 1);
pam1.init(), pam1.insert();
reverse(op + 1, op + n + 1);
pam2.init(), pam2.insert();

while (q--) {
int l, r, len;
scanf("%d%d", &l, &r), len = r - l + 1;
r = pam1.suf[r], l = pam2.suf[n - l + 1];
for (int i = 19; i >= 0; i--) {
if (pam2.nd[pam2.gr[l][i]].len > len) l = pam2.gr[l][i];
if (pam1.nd[pam1.gr[r][i]].len > len) r = pam1.gr[r][i];
}
if (pam2.nd[l].len > len) l = pam2.gr[l][0];
if (pam1.nd[r].len > len) r = pam1.gr[r][0];
if (pam2.nd[l].len == len) {
printf("YES\n");
goto tag;
}
for (int i = l; i; i = pam2.slink[i]) {
for (int j = r; j; j = pam1.slink[j]) {
int res = pam2.nd[i].len + pam1.nd[j].len - len;
int d1 = pam2.diff[i], d2 = pam1.diff[j];
int d = gcd(d1, d2), x, y;
if (res % d == 0) {
res /= d, d1 /= d, d2 /= d;
exGcd(d1, d2, x, y);
x *= res, y *= res;
int down1 = ceil(1.0 * (y - pam1.sdep[j] + 1) / d1);
int up1 = floor(1.0 * y / d1);
int down2 = ceil(1.0 * (-x) / d2);
int up2 = floor(1.0 * (pam2.sdep[i] - x - 1) / d2);
if (up1 < down1 || up2 < down2) continue;
if (down1 > up2 || down2 > up1) continue;
printf("YES\n");
goto tag;
}
}
}
printf("NO\n");
tag:;
}
return 0;
}