int k = 0, i = 0, j = 1;
while (k < n && i < n && j < n)
{
if (s[(i + k) % n] == s[(j + k) % n])
++k;
else
{
s[(i + k) % n] > s[(j + k) % n] ? i = i + k + 1 : j = j + k + 1;
if (i == j) i++;
k = 0;
}
}
i = Min(i, j);
for (int i = 2, j = 0; i <= m; ++i)
{
while (j > 0 && b[j + 1] != b[i])
j = nxt[j];
if (b[j + 1] == b[i])
++j;
nxt[i] = j;
}
for (int i = 1, j = 0; i <= n; ++i)
{
while (j > 0 && b[j + 1] != a[i])
j = nxt[j];
if (b[j + 1] == a[i])
++j;
if (j == m)
++cnt, j = nxt[j];
}
证明 记 a = m , b = n − m a = m, b = n - m a=m,b=n−m,不妨令 a ≥ b a \ge b a≥b,由作图易知,该串长度为 ( a m o d b ) + b (a \mod b) + b (amodb)+b 的后缀存在长度为 a m o d b a \mod b amodb 和 b b b 的循环节,因此可不断递归下去,其过程与求解最大公约数的过程相同,故最终可得到该串存在 gcd ( m , n − m ) \gcd(m, n - m) gcd(m,n−m) 的周期。
证明 不妨设循环节长度不为其整数倍,由作图易知,最小循环节满足 结论2 的前提,因而可推出其包含更小的周期,与最小循环节的定义矛盾。
inline void buildFail()
{
for (int i = 0; i < 26; ++i)
g[0][i] = 1;
que[qr = 1] = 1;
for (int i = 1, x, y, v; i <= qr; ++i)
{
x = que[i];
for (int j = 0; j < 26; ++j)
{
v = fail[x];
while (!g[v][j]) v = fail[v];
v = g[v][j];
y = g[x][j];
if (y) fail[y] = v, que[++qr] = y;
else g[x][j] = v, fg[x][j] = true;
// fg 表示是否是原 Trie树 上的边
}
}
}
scanf("%s", t + 1);
m = strlen(t + 1);
s[n = 0] = '@';
for (int i = 1; i <= m; ++i)
{
s[++n] = '#';
s[++n] = t[i];
}
s[++n] = '#';
s[n + 1] = '&';
inline void Manacher(char *s)
{
int p = 0, mx = 0;
for (int i = 1; i <= n; ++i)
{
r[i] = mx > i ? Min(r[(p << 1) - i], mx - i) : 1;
while (s[i - r[i]] == s[i + r[i]])
++r[i];
if (i + r[i] > mx)
mx = i + r[i], p = i;
}
}
z[1] = m;
for (int i = 2, l = 1, r = 1; i <= tm; ++i)
if (i <= r && z[i - l + 1] < r - i + 1)
z[i] = z[i - l + 1];
else
{
z[i] = Max(0, r - i + 1);
while (i + z[i] <= tm && t[i + z[i]] == t[z[i] + 1])
++z[i];
if (i + z[i] - 1 > r)
l = i, r = i + z[i] - 1;
}
const int N = 1e6 + 5;
int rank[N], height[N], sa[N], w[N];
int n, r; char s[N];
inline bool Equal(int *x, int a, int b, int k)
{
if (x[a] != x[b])
return false;
else
{
int p = a + k > n ? -1 : x[a + k],
q = b + k > n ? -1 : x[b + k];
return p == q;
}
}
inline void initSA()
{
int *x = rank, *y = height;
r = 255;
for (int i = 1; i <= n; ++i)
++w[s[i]];
for (int i = 2; i <= r; ++i)
w[i] += w[i - 1];
for (int i = n; i >= 1; --i)
sa[w[s[i]]--] = i;
x[sa[1]] = r = 1;
for (int i = 2; i <= n; ++i)
x[sa[i]] = s[sa[i - 1]] == s[sa[i]] ? r : ++r;
for (int k = 1; r < n; k <<= 1)
{
int yn = 0;
for (int i = n - k + 1; i <= n; ++i)
y[++yn] = i;
for (int i = 1; i <= n; ++i)
if (sa[i] > k)
y[++yn] = sa[i] - k;
for (int i = 1; i <= r; ++i)
w[i] = 0;
for (int i = 1; i <= n; ++i)
++w[x[y[i]]];
for (int i = 2; i <= r; ++i)
w[i] += w[i - 1];
for (int i = n; i >= 1; --i)
sa[w[x[y[i]]]--] = y[i];
std::swap(x, y);
x[sa[1]] = r = 1;
for (int i = 2; i <= n; ++i)
x[sa[i]] = Equal(y, sa[i - 1], sa[i], k) ? r : ++r;
}
for (int i = 1; i <= n; ++i)
rank[i] = x[i];
for (int i = 1, j, k = 0; i <= n; ++i)
{
if (rank[i] == 1)
continue ;
k ? --k : 0;
j = sa[rank[i] - 1];
while (i + k <= n && j + k <= n && s[i + k] == s[j + k]) ++k;
height[rank[i]] = k;
}
height[1] = 0;
}
int V_sam, last;
int sze[N2];
struct sam
{
int g[26];
int len, link;
inline void Clear()
{
memset(g, 0, sizeof(g));
len = link = 0;
}
}tr[N2];
inline void Init()
{
tr[V_sam = last = 1].Clear();
}
inline void Extend(char ch)
{
int cur = ++V_sam;
tr[cur].Clear();
ch -= 'a';
tr[cur].len = tr[last].len + 1;
sze[cur] = 1;
int p, q, clone;
for (p = last; p && !tr[p].g[ch]; p = tr[p].link)
tr[p].g[ch] = cur;
if (!p)
tr[cur].link = 1;
else
{
q = tr[p].g[ch];
if (tr[q].len == tr[p].len + 1)
tr[cur].link = q;
else
{
tr[clone = ++V_sam] = tr[q];
tr[cur].link = tr[q].link = clone;
tr[clone].len = tr[p].len + 1;
for (; p && tr[p].g[ch] == q; p = tr[p].link)
tr[p].g[ch] = clone;
}
}
last = cur;
}
证明 由 SAM \text{SAM} SAM 的构造方法可知,初始时有一个状态,前两次每次一定只会增加一个状态,之后每次至多只会增加两个状态,总状态数不超过 2 n − 1 2n - 1 2n−1。
证明 对于转移 ( p , q ) (p,q) (p,q),若 len ( p ) + 1 = len ( q ) \text{len}(p) + 1 =\text{len}(q) len(p)+1=len(q),则称该转移为连续的,否则为不连续的。考虑从 s t st st 开始的所有最长路径的生成树,生成树只包含连续的边,因此数量少于状态数,即连续的转移数不超过 2 n − 2 2n - 2 2n−2。
对于不连续的转移 ( p , q ) (p,q) (p,q),设其字符为 c c c,我们取它对应的字符串 s ′ = u + c + w s' = u + c + w s′=u+c+w, u u u 为初始状态到 p p p 的最长路径, w w w 为 q q q 到终止状态的最长路径。显然每个转移对应的字符串 s ′ s' s′ 是不同的,且 s ′ s' s′ 是原串 s s s 的后缀,因为 s s s 只有 n n n 个非空后缀且 s ′ s' s′ 一定不会取到 s s s,所以不连续转移数不超过 n − 1 n - 1 n−1。
因此我们可以得到转移数的上界 3 n − 3 3n - 3 3n−3,实际可以构造出的上界为 3 n − 4 3n - 4 3n−4。
for
语句和 c l o n e clone clone 复制转移。
for
语句和 c l o n e clone clone 复制转移,操作次数显然不会超过总转移数,因而是线性的。for
语句,可以证明, c u r cur cur 的后缀链接链是 l a s t last last 的后缀链接链通过一条 c h ch ch 出边所到达的状态组成的子集。设 l i l_i li 为 s [ 1 , i ] s[1,i] s[1,i] 对应状态的后缀链接链长, k i k_i ki 为添加第 i i i 个字符时的迭代次数,因此有 l i ≤ l i − 1 + 2 − k i l_i \le l_{i - 1}+2 - k_i li≤li−1+2−ki 且 l 0 = 0 l_0 = 0 l0=0,因此 0 < ∑ i = 1 n ( 2 − k i ) ≤ n 0 < \sum \limits_{i = 1}^{n}(2 - k_i) \le n 0<i=1∑n(2−ki)≤n,所以总迭代次数 ∑ i = 1 n k i \sum \limits_{i = 1}^{n}k_i i=1∑nki 是线性的。inline void Extend(int last, char ch)
{
cur = tr[last].g[ch];
tr[cur].len = tr[last].len + 1;
int p, q, clone;
for (p = tr[last].link; p && !tr[p].g[ch]; p = tr[p].link)
tr[p].g[ch] = cur; //p 应从 link(last) 开始循环
if (!p)
tr[cur].link = 1;
else
{
q = tr[p].g[ch];
if (tr[q].len == tr[p].len + 1)
tr[cur].link = q;
else
{
clone = ++V_sam;
tr[clone].link = tr[q].link;
tr[clone].len = tr[p].len + 1;
for (int j = 0; j < 26; ++j)
if (tr[q].g[j] && tr[tr[q].g[j]].len)
tr[clone].g[j] = tr[q].g[j];
//注意复制结点的转移边时不能复制是原字典树上但不是当前后缀自动机上的转移边
tr[cur].link = tr[q].link = clone;
for (; p && tr[p].g[ch] == q; p = tr[p].link)
tr[p].g[ch] = clone;
}
}
}
inline void buildGSAM()
{
for (int j = 0; j < 26; ++j)
if (tr[1].g[j])
que[++qr] = std::make_pair(1, j);
for (int i = 1, u, v, x; i <= qr; ++i)
{
u = que[i].first, v = que[i].second;
Extend(u, v);
x = tr[u].g[v];
for (int j = 0; j < 26; ++j)
if (tr[x].g[j])
que[++qr] = std::make_pair(x, j);
}
}
证明 因为总结点数为 O ( L ) \mathcal O(L) O(L),对于任意一个串 S i S_i Si,它对答案的贡献为 O ( min { ∣ S i ∣ 2 , L } ) \mathcal O(\min\{|S_i|^2,L\}) O(min{∣Si∣2,L})。
- 若 ∣ S i ∣ > L |S_i| > \sqrt L ∣Si∣>L,这样的串的数量不会超过 O ( L ) \mathcal O(\sqrt L) O(L) ,总复杂度 O ( L L ) \mathcal O(L\sqrt L) O(LL)。
- 若 ∣ S i ∣ ≤ L |S_i| \le \sqrt L ∣Si∣≤L,总复杂度 O ( ∑ i = 1 n ∣ S i ∣ 2 ) = O ( L max i = 1 n { ∣ S i ∣ } ) = O ( L L ) \mathcal O(\sum \limits_{i = 1}^{n}|S_i|^2)=\mathcal O(L\max\limits_{i=1}^{n}\{|S_i|\})=\mathcal O(L\sqrt L) O(i=1∑n∣Si∣2)=O(Li=1maxn{∣Si∣})=O(LL)。
struct Seq_AM
{
int lst[52], T;
node tr[N];
inline void Init()
{
T = 1;
for (int i = 0; i < 52; ++i)
lst[i] = 1;
}
inline void Insert(char ch)
{
int c = islower(ch) ? ch - 'a' + 26 : ch - 'A', x;
tr[x = ++T].par = lst[c];
for (int i = 0; i < 52; ++i)
for (int j = lst[i]; j && !tr[j].ch[c]; j = tr[j].par)
tr[j].ch[c] = x;
lst[c] = x;
}
};
namespace pam
{
int Vp, lenp, last;
int cnt[N], dep[N];
// cnt[x] 记录结点 x 代表回文子串的出现次数,deo[x] 表示结点 x 在回文树中的深度
int ch[N][26], len[N], fail[N];
char s[N];
inline int newNode(int l)
{
++Vp;
memset(ch[Vp], 0, sizeof(ch[Vp]));
len[Vp] = l;
fail[Vp] = cnt[Vp] = dep[Vp] = 0;
return Vp;
}
inline void Clear()
{
Vp = -1;
last = 0;
s[lenp = 0] = '$'; // 避免访问越界
newNode(0);
newNode(-1);
fail[0] = 1; // fail[偶根] -> 奇根
}
inline int getFail(int x)
{
while (s[lenp - len[x] - 1] != s[lenp])
x = fail[x];
return x;
}
inline void Extend(char c)
{
s[++lenp] = c;
int now = getFail(last);
if (!ch[now][c - 'a'])
{
int x = newNode(len[now] + 2);
fail[x] = ch[getFail(fail[now])][c - 'a'];
dep[x] = dep[fail[x]] + 1;
ch[now][c - 'a'] = x;
}
last = ch[now][c - 'a'];
++cnt[last];
}
inline void initTree()
{
for (int i = Vp; i >= 2; --i)
cnt[fail[i]] += cnt[i];
}
}
总回文子串的个数即所有结点在回文树上的深度之和(设奇根和偶根的深度均为 0)。
本质不同的回文子串个数即回文自动机除去奇根、偶根的结点数。
namespace pam
{
int Vp, lp, rp, lastl, lastr;
ll tot;
int cnt[N], dep[N];
int ch[N][26], len[N], fail[N];
char s[N];
inline int newNode(int l)
{
++Vp;
memset(ch[Vp], 0, sizeof(ch[Vp]));
len[Vp] = l;
fail[Vp] = cnt[Vp] = dep[Vp] = 0;
return Vp;
}
inline void Clear()
{
Vp = -1;
tot = 0;
lastl = lastr = 0;
for (int i = 0; i < N; ++i)
s[i] = '$';
lp = N + 1 >> 1;
rp = lp - 1;
newNode(0);
newNode(-1);
fail[0] = 1; // fail[even root] -> odd root
}
inline int getFailL(int x)
{
while (s[lp + len[x] + 1] != s[lp])
x = fail[x];
return x;
}
inline int getFailR(int x)
{
while (s[rp - len[x] - 1] != s[rp])
x = fail[x];
return x;
}
inline void ExtendL(char c)
{
s[--lp] = c;
int now = getFailL(lastl);
if (!ch[now][c - 'a'])
{
int x = newNode(len[now] + 2);
fail[x] = ch[getFailL(fail[now])][c - 'a'];
dep[x] = dep[fail[x]] + 1;
ch[now][c - 'a'] = x;
}
lastl = ch[now][c - 'a'];
tot += dep[lastl];
++cnt[lastl];
if (len[lastl] == rp - lp + 1)
lastr = lastl;
}
inline void ExtendR(char c)
{
s[++rp] = c;
int now = getFailR(lastr);
if (!ch[now][c - 'a'])
{
int x = newNode(len[now] + 2);
fail[x] = ch[getFailR(fail[now])][c - 'a'];
dep[x] = dep[fail[x]] + 1;
ch[now][c - 'a'] = x;
}
lastr = ch[now][c - 'a'];
tot += dep[lastr];
++cnt[lastr];
if (len[lastr] == rp - lp + 1)
lastl = lastr;
}
inline void initTree()
{
for (int i = Vp; i >= 2; --i)
cnt[fail[i]] += cnt[i];
}
}
若题目要求(或可通过一定的转化)将原串划分若干个具有限制的回文串,且 DP 的转移形式大致形如
f i = ( ∑ s [ j + 1 … i ] 是回文串 f j ) + C 或 f i = min s [ j + 1 … i ] 是回文串 / max s [ j + 1 … i ] 是回文串 { f j } + C f_i = \left(\sum\limits_{s[j+1\dots i]是回文串}f_j \right)+ C\ \ 或\ \ f_i = \min\limits_{s[j+1\dots i]是回文串} /\max\limits_{s[j+1\dots i]是回文串}\{f_j\} + C fi= s[j+1…i]是回文串∑fj +C 或 fi=s[j+1…i]是回文串min/s[j+1…i]是回文串max{fj}+C
则可通过回文树将每次转移的复杂度优化至 O ( log ∣ s ∣ ) \mathcal O(\log |s|) O(log∣s∣)。
周期 若 0 < p ≤ ∣ s ∣ , ∀ 1 ≤ i ≤ ∣ s ∣ − p , s [ i ] = s [ i + p ] 0 < p \le |s|,\forall 1 \le i \le |s| - p, s[i] = s[i + p] 0<p≤∣s∣,∀1≤i≤∣s∣−p,s[i]=s[i+p],则 p p p 为 s s s 的周期。
border 若 0 ≤ r < ∣ s ∣ 0\le r < |s| 0≤r<∣s∣, s [ 1 … r ] = s [ ∣ s ∣ − r + 1 … ∣ s ∣ ] s[1\dots r] = s[|s| - r + 1\dots|s|] s[1…r]=s[∣s∣−r+1…∣s∣],则 s [ 1 … r ] s[1\dots r] s[1…r] 是 s s s 的 border。
由周期和 border 的定义不难证明, t t t 是 s s s 的 border,当且仅当 ∣ s ∣ − ∣ t ∣ |s| - |t| ∣s∣−∣t∣ 是 s s s 的周期。
由该结论及回文串的对称性可顺序推导出下面四条引理:
证明(前三条引理的证明较为容易,这里仅证明 引理4)
- 由 引理3, ∣ u ∣ = ∣ x ∣ − ∣ y ∣ |u| = |x| - |y| ∣u∣=∣x∣−∣y∣ 是 x x x 的最小周期, ∣ v ∣ = ∣ y ∣ − ∣ z ∣ |v| = |y| - |z| ∣v∣=∣y∣−∣z∣ 是 y y y 的最小周期。假设 ∣ u ∣ < ∣ v ∣ |u|<|v| ∣u∣<∣v∣,则因为 ∣ u ∣ |u| ∣u∣ 也是 y y y 的周期,与 ∣ v ∣ |v| ∣v∣ 是 y y y 的最小周期矛盾。
- 如下图所示,因为 y y y 是 x x x 的 border,所以 v v v 是 x x x 的前缀,设字符串 x = v w x = vw x=vw,所以 z z z 是 w w w 的 border。假设 ∣ u ∣ ≤ ∣ z ∣ |u| \le |z| ∣u∣≤∣z∣,那么 ∣ z u ∣ ≤ 2 ∣ z ∣ |zu| \le 2|z| ∣zu∣≤2∣z∣,由 引理2,因为 z z z 是回文串所以 w w w 是回文串,又因为 ∣ u ∣ > ∣ v ∣ |u|>|v| ∣u∣>∣v∣,所以 ∣ w ∣ > ∣ y ∣ |w|>|y| ∣w∣>∣y∣,与 y y y 是 x x x 的最长真回文后缀矛盾。
- u , v u,v u,v 都是 x x x 的前缀, ∣ u ∣ = ∣ v ∣ |u|=|v| ∣u∣=∣v∣,所以 u = v u = v u=v。
结论1 将 s s s 的所有回文后缀按长度排序后,可划分成 O ( log ∣ S ∣ ) \mathcal O(\log |S|) O(log∣S∣) 段等差数列。
证明 设 s s s 的所有回文后缀长度从小到大排序为 l 1 , l 2 , … , l k l_1,l_2,\dots,l_k l1,l2,…,lk,对于相邻两段等差数列的分界处,有 l i − l i − 1 ≠ l i + 1 − l i l_i - l_{i - 1} \not = l_{i + 1} - l_i li−li−1=li+1−li,由 引理4,必有 l i + 1 − l i > l i − l i − 1 l_{i+1} - l_i > l_i - l_{i - 1} li+1−li>li−li−1 且 l i + 1 − l i > l i − 1 l_{i + 1} - l_{i} > l_{i - 1} li+1−li>li−1,即 l i + 1 > 2 l i − 1 l_{i + 1} > 2l_{i - 1} li+1>2li−1,因而在相邻两段等差数列的分界处,回文后缀的长度至少翻一倍,显然至多只能翻倍 O ( log ∣ s ∣ ) \mathcal O(\log |s|) O(log∣s∣) 次,原命题得证。
证明 由 引理1, fail ( x ) \text{fail}(x) fail(x) 是 x x x 的 border,因而在 i − dif ( x ) i - \text{dif}(x) i−dif(x) 处出现。
由于 x x x 与 fail ( x ) \text{fail}(x) fail(x) 属于同一等差数列,则 2 len ( fail ( x ) ) ≥ len(x) 2\text{len}(\text{fail}(x)) \ge \text{len(x)} 2len(fail(x))≥len(x),假设 fail ( x ) \text{fail}(x) fail(x) 在 ( i − dif ( x ) , i ) (i - \text{dif}(x),i) (i−dif(x),i) 中的某位置出现,则该位置的 fail ( x ) \text{fail}(x) fail(x) 与 i − dif ( x ) i - \text{dif}(x) i−dif(x) 处的 fail ( x ) \text{fail}(x) fail(x) 必然有交,设交集为 w w w,且 fail ( x ) = v w \text{fail}(x) = vw fail(x)=vw。由 引理1, w w w 是 fail ( x ) \text{fail}(x) fail(x) 的 border,推出 w w w 是 fail(x) \text{fail(x)} fail(x) 的回文串。则该位置的 fail ( x ) \text{fail}(x) fail(x) 与 i − dif ( x ) i - \text{dif}(x) i−dif(x) 处的 fail ( x ) \text{fail}(x) fail(x) 的并集 v w v ′ ( v ′ 为 v 倒序得到的字符串 ) vwv'(v'为v倒序得到的字符串) vwv′(v′为v倒序得到的字符串) 也为回文串且是 x x x 的前缀,显然有 ∣ v w v ′ ∣ > len ( fail ( x ) ) |vwv'| > \text{len}(\text{fail}(x)) ∣vwv′∣>len(fail(x)),与 fail ( x ) \text{fail}(x) fail(x) 是最长回文真前(后)缀矛盾。
由 结论2 可知:
以求回文划分数为例,核心代码如下:
inline void Extend(char c)
{
s[++lenp] = c;
int now = getFail(last);
if (!ch[now][c - 'a'])
{
int x = newNode(len[now] + 2);
fail[x] = ch[getFail(fail[now])][c - 'a'];
ch[now][c - 'a'] = x;
dif[x] = len[x] - len[fail[x]];
if (dif[x] == dif[fail[x]])
slink[x] = slink[fail[x]];
else
slink[x] = fail[x];
}
last = ch[now][c - 'a'];
}
inline void calc()
{
f[0] = 1;
for (int i = 1; i <= m; ++i)
{
pam::Extend(t[i]);
for (int x = last; x > 1; x = slink[x])
{
g[x] = f[i - len[slink[x]] - dif[x]];
if (fail[x] != slink[x])
add(g[x], g[fail[x]]);
add(f[i], g[x]);
}
}
}