后缀排序
倍增算法
(n) 字符串的长度。
(m) 当前后缀(离散化后)的值域。对于char可以跳过离散化,初值取128即可,对于int要离散化,初值取n即可,初值要保证覆盖整个值域。
(sa[i]) 排名为 (i) 的后缀的起始位置。
(rk[i]) 起始位置为 (i) 的后缀的排名。
验证:https://www.luogu.com.cn/problem/P3809
const int MAXN = 1000000 + 10;
int n, m, ct[MAXN], tp[MAXN];
int sa[MAXN], rk[MAXN], ht[MAXN];
void RadixSort() {
for(int i = 0; i <= m; ++i)
ct[i] = 0;
for(int i = 1; i <= n; ++i)
++ct[rk[i]];
for(int i = 1; i <= m; ++i)
ct[i] += ct[i - 1];
for(int i = n; i >= 1; --i)
sa[ct[rk[tp[i]]]--] = tp[i];
}
bool Compare(int i, int j, int l) {
if(tp[sa[i]] == tp[sa[j]]) {
if(sa[i] + l <= n && sa[j] + l <= n) {
if(tp[sa[i] + l] == tp[sa[j] + l])
return 1;
}
}
return 0;
}
void SuffixSort(char *s) {
n = strlen(s + 1), m = 128;
for(int i = 1; i <= n; ++i) {
rk[i] = s[i];
tp[i] = i;
}
RadixSort();
for(int l = 1;; l <<= 1) {
m = 0;
for(int i = n - l + 1; i <= n; ++i)
tp[++m] = i;
for(int i = 1; i <= n; ++i) {
if(sa[i] > l)
tp[++m] = sa[i] - l;
}
RadixSort();
swap(tp, rk);
m = 1;
rk[sa[1]] = 1;
for(int i = 2; i <= n; ++i) {
if(Compare(i - 1, i, l) == 0)
++m;
rk[sa[i]] = m;
}
if(m == n)
break;
}
}
最小循环表示
把字符串S循环移动,找字典序最小的那个表示。
把字符串 (S) 复制变成字符串 (S+S) ,然后变成后缀排序的问题。前[1,n]的后缀数组中的最小的那个就是答案。当然可能会有多个循环表示的串都是代表同一个东西的,这样要注意题目的特殊限制,也可以利用这个最小循环的串构造出来之后在 (S+S) 中查找第一个匹配位置。
验证:https://www.luogu.com.cn/problem/P4051
这一题只需要把最小表示串找出来,所以就找任意一个即可。注意n的取值要和后缀数组中字符串相匹配。
int n = strlen(str + 1);
for(int i = 1; i <= n; ++i)
str[i + i] = str[i];
str[n + n + 1] = ' ';
SuffixSort(str);
int pos = min_element(rk + 1, rk + 1 + n) - rk;
for(int i = 1; i <= n; ++i)
putchar(str[pos + i - 1]);
putchar('
');
假如字符集很大,那么后缀自动机就会失效,这个时候后缀数组可以通过离散化解决,然后字符串取离散化后的结果,m初始值就取n(而不是128)。
所有循环表示
找出S的所有循环表示,并把他们按字典序排列。
验证:https://www.luogu.com.cn/problem/P4051
对于后缀数组来说,这个问题和上面的一模一样,直接取前n个的rk重新排序就行。
int n = strlen(str + 1);
for(int i = 1; i <= n; ++i)
str[i + n] = str[i];
SuffixSort(str);
for(int i = 1; i <= n; ++i)
p[i] = {rk[i], i};
sort(p + 1, p + 1 + n);
for(int i = 1; i <= n; ++i)
putchar(str[p[i].second + n - 1]);
putchar('
');
在文本串S中查找模式串T的所有出现位置
验证:https://www.luogu.com.cn/problem/P3375
对S构造后缀数组,然后在后缀数组sa上面二分,二分枚举到一个位置M,排名为M的后缀的起始位置是sa[M],然后对sa[M]的后缀和模式串T暴力比较,得出的结果可能是M偏大、M偏小,或者刚刚好,找到第一个刚刚好的位置,然后同理找到最后一个刚刚好的位置,中间的就是所有的出现次数。因为寻找的是以T开头的所有后缀的起始位置,所以他们必定是在后缀数组中连续的一个区间。
int sl, tl;
char s[MAXN];
char t[MAXN];
int Check(int pos) {
return strncmp(s + sa[pos], t + 1, tl);
}
int FirstEqual() {
int L = 1, R = sl;
while(L < R) {
int M = (L + R) / 2;
if(Check(M) >= 0)
R = M;
else
L = M + 1;
}
return Check(L) == 0 ? L : sl + 1;
}
int LastEqual() {
int L = 1, R = sl;
while(L < R) {
int M = (L + R + 1) / 2;
if(Check(M) <= 0)
L = M;
else
R = M - 1;
}
return Check(L) == 0 ? L : 0;
}
vi GetAllOccurences() {
sl = strlen(s + 1);
tl = strlen(t + 1);
SuffixSort(s);
int L = FirstEqual();
int R = LastEqual();
vi ans;
for(int i = L; i <= R; ++i)
ans.eb(sa[i]);
srt(ans);
return ans;
}
最长公共前缀
下面的约定中,lcp的参数是字符串,并且用i表示suf(i),sa[i]表示第i名的后缀,即suf(sa[i])
lcp(i,j)=lcp(j,i)
lcp(i,i)=len(suf(sa[i]))=n-sa[i]+1
$lcp(sa[i],sa[j])=minlimits_{kin[i+1,j]}(lcp(sa[k],sa[k-1])) $
即两个相隔甚远的后缀的lcp可以用相邻后缀的lcp的rmq求出来。
设 ht[i]=lcp(sa[i],sa[i-1]) ht[1]=0 即第i名的后缀和它前1名的LCP的长度。
那么 (lcp(sa[i],sa[j])=minlimits_{kin[i+1,j]}ht[k])
那么ht[rk[i]]>=ht[rk[i-1]]-1
求ht数组 代码
然后,求两个后缀的lcp就变成RMQ问题,可以用ST表加速。
比较子串的字典序
子串A[a,b] 和B[c,d]
若lcp(sa[a],sa[c])>=min(|A|,|B|) ,则A<B等价于|A|<|B|
否则,A<B等价于rk[a]<rk[c]
贪心取字符串的首尾组成字典序最小的新串:比较子串s和子串t的反串的字典序,对S+'#'+S构造后缀数组。
在字符串T中查找子串S
对T构造后缀数组,然后在T的后缀数组上二分,暴力匹配S。这是一个在线的算法,比起AC机的优势。单次匹配复杂度可能优于KMP。
本质不同子串的数目
验证:https://www.luogu.com.cn/problem/P2408
所有的子串一共有(frac{1}{2}n(n+1))个,其中对于每个后缀,重复的恰好是ht的数量。
(frac{1}{2}n(n+1)-sumlimits_{i=2}^{n} ht[i])
出现至少k次的子串的最大长度
出现至少k次,意味着至少连续k个后缀的LCP是这个串。
故是连续k-1个ht的最小值,枚举所有的最小值求最大值。
https://codeforces.com/contest/149/problem/E
题意:给定n=1e5长的字符串S,t=100次询问,每次询问m=1e3长的字符串T,问是否可以从S中选择两个不相交的非空子串s1,s2使得s1+s2=T。有一个哈希的做法,不过TLE22了,枚举s1的长度那么可以直接算出s2的长度,总共有1e3种长度,在S上跑1e3次尺取哈希,然后用数据结构找出第一个哈希值等于s1的位置和最后一个哈希值等于s2的位置比较,复杂度最差为O(nmt)。改用后缀数组的优势在于不再需要跑原字符串的长度,构造的复杂度为nlogn,然后类似“找字符串T的所有出现位置”的做法,找出那个连续区间然后套ST表查出来,复杂度是O(nlogn+tmlogn)。ST表中传入的数组a为后缀数组sa。