(背的
S
A
SA
SA是从
0
0
0到
n
n
n的)
几个数组的意义:
s a [ i ] sa[i] sa[i]表示排名为 i i i的后缀的首字母位置。
t [ i ] t[i] t[i]表示后缀首字母在 i i i这个位置的第一关键字的排名。
t 2 [ i ] t2[i] t2[i]表示第二关键字排名为 i i i的后缀的首字母位置。
h e i g h t [ i ] height[i] height[i] 表示排名为 i i i和 i − 1 i-1 i−1的两个后缀的 L C P LCP LCP
板子标准代码:
inline void build_sa(int m) {
for (int i = 0; i < m; ++i) c[i] = 0;
for (int i = 0; i < n; ++i) c[t[i] = s[i]]++;
for (int i = 1; i < m; ++i) c[i] += c[i - 1];
for (int i = n - 1; i >= 0; --i) sa[--c[t[i]]] = i;
for (int k = 1; k <= n; k <<= 1) {
int p = 0;
for (int i = n - k; i < n; ++i) t2[p++] = i;
for (int i = 0; i < n; ++i) if (sa[i] >= k) t2[p++] = sa[i] - k;
for (int i = 0; i < m; ++i) c[i] = 0;
for (int i = 0; i < n; ++i) c[t[t2[i]]]++;
for (int i = 1; i < m; ++i) c[i] += c[i - 1];
for (int i = n - 1; i >= 0; --i) sa[--c[t[t2[i]]]] = t2[i];
swap(t, t2);
p = 1; t[sa[0]] = 0;
for (int i = 1; i < n; ++i) {
t[sa[i]] = ((t2[sa[i - 1]] == t2[sa[i]] && (sa[i - 1] + k < n ? t2[sa[i - 1] + k] : -1) == (sa[i] + k < n ? t2[sa[i] + k] : -1)) ? (p - 1) : p++);
}
if (p >= n) break;
m = p;
}
}
inline void build_hei() {
int j, k = 0;
for (int i = 0; i < n; ++i) rnk[sa[i]] = i;
for (int i = 0; i < n; ++i) {
if (!rnk[i]) continue;
if (k) k--;
j = sa[rnk[i] - 1];
while (s[j + k] == s[i + k]) k++;
hei[rnk[i]] = k;
}
}
inline void predo() {
for (int i = 0; i < n; ++i) f[i][0] = hei[i];
for (int j = 1; (1 << j) <= n; ++j) {
for (int i = 0; i + (1 << j) - 1 < n; ++i) {
f[i][j] = min(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
}
}
}
inline int LCP(int l, int r) {
int a = rnk[l], b = rnk[r];
if (a > b) swap(a, b); a++;
int k = (int)log2(b - a + 1);
return min(f[a][k], f[b - (1 << k) + 1][k]);
}
一些套路
一:后缀的LCP就是两个完全相等的子串
二:求最长可重叠重复子串——求height的最大值
三:求最长不可重叠重复子串——二分长度k,因为LCP是区间height的RMQ。所以扫一遍,按照区间分组。一旦有一个height<k了,说明这一组不能要。就在这一组处断开。对于每一组,求一求里面SA的最大值和最小值。可以在扫一遍的时候完成。
四:求最长可重叠k重复子串——二分长度t。同样分成若干组,考察每一组的大小是否有大于等于k的就可以了。
五:求不相同的子串的个数——按照排名降低的顺序处理。即(sa[1],sa[2],sa[3])的顺序处理。可以发现每加入一个后缀,它会产生n-sa[i]+1个前缀。但是其中有height[i]个是相同的。所以此次对答案的贡献就是n-sa[i]+1-height[i]。所有加起来就好了。
六:最长回文子串。 ①manacher。 ②把原来的串翻转接到原串后面(注意中间用一个特殊字符隔开)。枚举原来的每一个中心位置。就是求LCP的最大值了。实现起来有点细节。
七:给一个多次重复而来的一个串,求(不可重叠)最多重复次数。 枚举重复长度k,先判断能否整除。如果可以整除的话再判断LCP(0,k)是不是n-k。由于0是固定的。所以预处理的时候只需要预处理每个数到rnk[0]+1这个位置的最小值就可以了。可以从rnk[0]+1这个位置向外扫。
*八:对于随意一个串(不保证是重复而来),求(可重叠)最多重复次数。 枚举长度L。考虑答案一定有2个对应的位置(距离为L,字符相等)。枚举这N/L个可能位置,与它前面的那个位置形成一个可能对。分别向前向后求LCP。假设长度为k,那么此时的长度为K/L+1. 说明:这个东西的重复性比较显然,LCP就代表了重复性。连续性可以考虑有相交。(不是特别理解)
九:最长公共子串。两个字符串连接起来,中间用一个不可能出现的字符连接。仿照第二问的思路,不过要特殊要求两个不在同一边。这个查询一下sa就好了。
十:给两个串,求长度不小于k的公共子串的个数。 先连起来。考虑如果没有长度的限制且没有必须在两边的限制,就是求所有的LCP的和。可以使用单调栈O(n)维护出来。(后面有这样的题) 有长度的限制的话,按照height分组。每一组分别做。最后再加起来就好了。有两边的限制的话,一种方法是容斥,求三个SA。另外一个方法是就考虑加入每个B的后缀,对A有什么贡献。用单调栈这样维护A。然后再反过来维护B就好了。具体操作起来(以加入B为例),按照rnk的顺序枚举,sa可以指引当前这个是B的还是A的。如果是A的就插到栈里面,如果是B的就弹出那些大于B的元素。统计一下答案。(注意这里统计要记录B的下标差,不能A,B的下标混着统计)
十一:在至少
k
k
k个字符串中出现过的最长子串。 把这若干个字符窜用不同的没有出现过的字符连接起来,求一个SA。二分长度,按照height的rnk顺序分组。观察他们的SA定位的位置是否在至少k个区间里。
十二:求每个字符串至少出现两次且不重叠的最长子串 。连起来,求SA,二分长度分组。看每组的是否存在两个相同的height且在原来的同一字符串中且他们对应的SA对应回去是距离大于长度的。扫的时候用一个桶记录。
十三:求 出现或反转后出现在每个字符串中的最长子串。先把每个字符串翻转接到后面再连起来。二分答案分组height。考察是否有一组里面包含了所有的原区间或者翻转后的区间。
题题题
l
u
o
g
u
P
3966
luoguP3966
luoguP3966
①:一开始把这些串拼接起来,然后求一个前缀哈希值。对于每个串,枚举它开始的匹配的位置。用前缀hash值算一下就知道是否匹配了。
②:把hash判断改成用sa判断。求两个后缀的LCP看它是不是长度大于等于匹配串的长度。注意中间要用最大的字符链接。
③:优化:因为LCP是求区间RMQ,所以对于每一个串。向左向右扩展。能扩展到的最远的长度就是答案。暴力是最坏O(n^2),二分是O(nlogn)。正确性说明,考虑②的过程,也是对于一个固定的模式串,枚举所有可行的起点。这里只不过是按照
r
n
k
rnk
rnk的从近到远顺序枚举的。减少了很多冗余。
④:如果是写二分的话,由于height不是单调的。所以要用ST表维护区间最小值了。
l
u
o
g
u
P
4248
luoguP4248
luoguP4248
发现左边式子可以直接算。就只要算任意两队LCP的和就好了。
f
[
i
]
f[i]
f[i]表示
i
i
i为右端点的答案,发现LCP是区间
h
e
i
g
h
t
height
height的最小值。所以维护一个单调栈表示最近的
h
e
i
g
h
t
[
j
]
<
=
h
e
i
g
h
t
[
i
]
height[j]<=height[i]
height[j]<=height[i]的地方。
[
j
+
1
,
i
]
[j+1,i]
[j+1,i]的答案都是
h
e
i
g
h
t
[
i
]
height[i]
height[i]。所以转移就是
f
[
i
]
+
=
f
[
j
]
+
(
i
−
j
)
∗
h
e
i
g
h
t
[
i
]
f[i]+=f[j] + (i-j)*height[i]
f[i]+=f[j]+(i−j)∗height[i]。每次
a
n
s
+
=
f
[
i
]
ans+=f[i]
ans+=f[i]就好了。
i
,
j
i,j
i,j任意需要
∗
2
*2
∗2。
l
u
o
g
u
P
3181
luoguP3181
luoguP3181
①:
O
(
n
2
)
O(n^2)
O(n2)暴力,枚举两个子串的开头。求一个后缀的LCP,得到的就是那个最长的子串。只计数包含两个开头端点的对数就可以。
②:这玩意也是一个LCP的和的形式,仿照上题利用单调栈维护就行。不过要注意这题的
i
,
j
i,j
i,j要分属左右两边。要么先算全部的,在分别减去
i
,
j
i,j
i,j都属于一边的。要算三遍SA。 要么在单调栈里面改改。
l u o g u P 3763 luoguP3763 luoguP3763
①:把两个字符串拼接起来,中间用没有出现过的最小字符’#'连接。
②:求完 S A , H e i g h t SA,Height SA,Height之后暴力。枚举匹配的首位,暴力记录当前有多少位不匹配。如果当前为不匹配就加1,否则指针就往后跳它们的 l c p lcp lcp这么多。。 O ( n l o g n ) O(nlogn) O(nlogn)