本文中用 (he) 代替 (height),(he_i=operatorname{lcp}(sa[i],sa[i-1]))。若无特殊说明,时间复杂度指除去求后缀数组的时间复杂度。
【模板】后缀排序
不同子串个数
(sa[i]) 有 (n-i+1) 个前缀,与 (sa[i-1]) 重复了 (he_i) 个,对答案的贡献就是 (n-i+1-he_i)。
(sumlimits_{i=1}^n n-i+1-he_i=n^2+n-dfrac{n(n+1)}{2}-sum he_i=dfrac{n(n+1)}{2}-sum he_i)
[USACO5.1] 乐曲主题Musical Themes
题意:求出现至少两次(不可重叠)的子串的最长长度。
转调其实就是让我们求一个差分数组。
考虑二分答案 (mid),如果 (he) 中存在都不小于 (mid) 的连续段 ([L,R]),此段中原串中最左边的位置(原串中,下同)与最右边的位置之差 (>=mid) 即 (max{sa[L-1..R]}-min{sa[L-1..R]}>=mid)(此题中应为 (>mid),因为若在差分数组中刚好相邻,原串中就会有重叠),说明(mid)合法。时间复杂度 (operatorname{O}(nlog n))。
[AHOI2013] 差异
先不管 lcp。长度为 (l) 的后缀出现在 (T_i) 的次数是 (l-1),出现在 (T_j) 的次数是 (n-l),那么总和是 (sumlimits_{l=1}^nl(l-1+n-l)=(n-1)sumlimits_{l=1}^nl=frac{1}{2}n(n-1)(n+1))。
接下来要求两两后缀的 lcp。我们知道 (operatorname{lcp}(i,j)=minlimits_{rnk_i<kle rnk_j}{he_k}),但即使用 st 表也是 (operatorname{O}(n^2)) 的。考虑每个 (he_i) 的贡献。用 单调栈 求出 (L_i,R_i),分别是 (i) 往前数、往后数第一个 (he) 值小于 (he_i)的,此时 (minlimits_{L_i<k<R_i}{he_k}=he_i),那么 (he_i) 对答案的贡献就是 (he_i(R_i-i)(i-L_i))。在原串中相当于 (sa[L_i..i-1]) 和 (sa[i..R_i-1]) 两两配对,(operatorname{lcp}) 都是 (he_i)。这一部分时间复杂度 (operatorname{O}(n))。
[USACO06DEC] Milk Patterns G
题意:求字符串中出现至少 (k) 次(可重叠)的子串的最长长度。
假设 (he) 中有区间 ([L,R])(代表原串中 (sa[L-1..R])),要求出现至少 (k) 次,需满足 (R-(L-1)+1ge k) 即 (R-L+1ge k-1)。如果区间 ([L,R]) 向左或向右扩展一点,(operatorname{lcp}) 一定不会增大。所以只需要对所有长度为 (k-1) 的区间的 (he) 最小值求最大值即可,用单调队列 (operatorname{O}(n)),st 表(operatorname{O}(nlog n))。
Long Long Message
题意:求两字符串的最长公共子串。
用 SA 解决多串问题时,可以考虑把字符串首尾相连。用特殊字符隔开(每个特殊字符互不相同)可以避免 lcp 横跨两个字符串的问题。
假设连接后字符串为 (S[1..n]),分隔字符下标为 (sep),题目转化成求 (maxlimits_{i<sep<j}{operatorname{lcp}(i,j)})。根据排名离得越远,(operatorname{lcp}) 不会更大,可知排名相邻(且来自不同字符串)的两个后缀的 (operatorname{lcp}) 才有可能是答案。
[SDOI2008]Sandy 的卡片
题意:求多个字符串的最长公共子串(此题需先差分)。
沿用上题的套路,把所有字符串拼接在一起(字符串间需用不同字符隔开),并记录第 (i) 个字符对应第几个字符串,记为 (id_i)。
然后二分答案,判定的标准是 (he) 中存在一段连续的、大于 (mid) 且每个字符串都在 (id) 中出现过(这个用桶来做)。
[SDOI2016]生成魔咒
题意:初始时有一空串,每次在末尾加入一个字符,并求出当前串的不同子串数。
SA 中插入不太好搞,反过来想,把字符串翻转,每次从开头删去一个字符。
如果一个字符被删去,我们称以这个字符开头的后缀被删去。记 (pre_i) 为排名在 (i) 之前且最大的后缀排名,(nxt_i) 同理(模拟一个链表)。求出 (he) 后,我们把 (he_i) 的定义改为 (operatorname{lcp}(pre_i,i))。
前面提到过长度为 (n) 的字符串的不同子串数为 (dfrac{n(n+1)}{2}-sum he_i)。维护一下当前的 (sum he_i),删去排名为 (i) 的后缀就等同于在链表中删去第 (i) 个节点。同时维护一下 (he_{nxt_i})(显然只会对这个产生影响)。
【模板】后缀自动机(SAM)
题意:求子串长度 ( imes) 出现次数的最大值。
首先不难想到这个子串的长度一定是 (he_i) 之一。
那么就可以用单调栈求出排名为 (i) 的子串出现了多少次。
以下题目都有一定难度,请谨慎食用
P3975 [TJOI2015]弦论
题意:求第 (k) 小子串,分相同子串算一个 / 多个 两种情况。
相同子串算一个的很好处理,这里就不说了。
相同子串算多个的情况用 SA 做确实有点复杂 不过既然写在这里肯定是可以哒
排名为 (i) 的后缀,有 (n-sa_i+1) 个前缀
再对这东西做一个前缀和,记为 (sum_i)
然后建一个 st 表。
考虑对 (he) 分组,要求每组只有第一个 (he) 是 (0)。
(he=0) 代表这两个后缀第一个字母都不相同,显然它们不会有任何前缀是相同的。那么就可以判断答案在哪个组里。
设这个组的左右端点(后缀排名)分别是 (L, R),要求的排名是 (k)。
先找到区间内 (min{operatorname{lcp}}) 的位置,记为 (mid)。
容易发现排名在 ([L,R]) 中的后缀,它们长度为 (he_{mid}) 的前缀都相同
于是可以判断答案是否在这些子串里,即判断 (k) 是否 (leq (R-L+1) imes he_{mid})。如果是,进而可以推出长度是多少。
如果不是,判断答案在 (mid) 左还是右(除去这些长度为 (he_{mid}))的子串
然后更新一下 (k,L,R) 就好了。
注意要记录上一次的 (he_{mid}),然后处理一些细节。
每次区间长度至少减少 (1),这一部分时间复杂度 (O(n))
inline int get_min(int x, int y) {
return he[x] < he[y] ? x : y;
}
/*
SA
*/
void solve0() {
rep(i, 1, n) {
if(k > n - sa[i] + 1 - he[i]) k -= n - sa[i] + 1 - he[i];
else {
print(sa[i], sa[i] + he[i] + k - 1);
return ;
}
}
puts("-1");
}
void build_st() {
rep(i, 1, n) sum[i] = sum[i - 1] + n - sa[i] + 1;
rep(i, 2, n) lg[i] = lg[i / 2] + 1;
rep(i, 1, n) st[i][0] = i;
rep(len, 1, lg[n]) rep(i, 1, n - (1 << len) + 1)
st[i][len] = get_min(st[i][len - 1], st[i + (1 << len - 1)][len - 1]);
}
inline ll get_sum(int l, int r) {
return sum[r] - sum[l - 1];
}
inline int query(int l, int r) {
l ++ ;
int k = lg[r - l + 1];
return get_min(st[l][k], st[r - (1 << k) + 1][k]);
}
void solve1() {
int l = 1, r, mid, now = 0;
ll tmp;
for(; l <= n; ++ l)
if(he[l] == 0) {
for(r = l; he[r + 1] > 0; ++ r) ;
if(k <= get_sum(l, r)) break;
k -= get_sum(l, r);
l = r;
}
while(l < r) {
mid = query(l, r);
tmp = 1ll * (r - l + 1) * (he[mid] - now); // 注意计算个数时要减去上一次的长度
if(he[mid] > now && k <= tmp) {
print(sa[l], sa[l] + now + (k - 1) / (he[mid] - now));
return ;
}
k -= tmp;
tmp = get_sum(l, mid - 1) - 1ll * he[mid] * (mid - l);
if(k <= tmp) r = mid - 1;
else k -= tmp, l = mid;
now = he[mid];
}
print(sa[l], sa[l] + now + k - 1);
}
signed main() {
scanf("%s", s + 1);
n = strlen(s + 1);
scanf("%d%d", &T, &k);
SA();
if(!T) solve0();
else if(1ll * n * (n + 1) / 2 < k) puts("-1");
else {
build_st();
solve1();
}
return 0;
}
[HEOI2016/TJOI2016]字符串
考虑二分答案。设当前答案为 (len)。
那么要判断是否存在一个后缀 (p),使得 (ale ple b-len+1) 且 (operatorname{lcp}(p,c)ge len)
考虑一个后缀的排名 (k)
(k<rnk_c) 时,如果 (k) 越小,(minlimits_{k<ile rnk_c}{he_i}) 单调不增。
(k>rnk_c) 时同理。
那么,使得 (operatorname{lcp}(p,c)ge len) 的后缀 (p) 的 排名 一定是一段区间。可以用 st 表加二分求出这个排名区间。
把下标看成第一维,排名看成第二维,变成了一个二维数点问题。再用一个主席树即可。
时间复杂度 (O(nlog^2n))。
int build(int l, int r) {
int p = ++ tot;
if(l == r) return p;
int mid = l + r >> 1;
lc[p] = build(l, mid);
rc[p] = build(mid + 1, r);
return p;
}
int modify(int rt, int l, int r, int t) {
int p = ++ tot;
sum[p] = sum[rt] + 1;
if(l == r) return p;
int mid = l + r >> 1;
if(t <= mid) {
lc[p] = modify(lc[rt], l, mid, t);
rc[p] = rc[rt];
} else {
rc[p] = modify(rc[rt], mid + 1, r, t);
lc[p] = lc[rt];
}
return p;
}
int query(int p, int l, int r, ci &tl, ci &tr) {
if(tl <= l && r <= tr) return sum[p];
int mid = l + r >> 1, res = 0;
if(tl <= mid) res += query(lc[p], l, mid, tl, tr);
if(mid < tr) res += query(rc[p], mid + 1, r, tl, tr);
return res;
}
inline int query(int x, int y, int l, int r) {
return query(rt[y], 1, n, l, r) - query(rt[max(x - 1, 0)], 1, n, l, r);
}
void solve() {
int a, b, c, d, rnkL, rnkR, lenl, lenr, len, ans = 0;
scanf("%d%d%d%d", &a, &b, &c, &d);
lenl = 0; lenr = min(d - c + 1, b - a + 1);
while(lenl <= lenr) {
len = lenl + lenr >> 1;
rnkL = rnkR = rnk[c];
per(i, lg[rnk[c] - 1], 0)
if(lcp(rnkL - (1 << i), rnk[c]) >= len)
rnkL -= 1 << i;
per(i, lg[n - rnk[c] + 1], 0)
if(lcp(rnk[c], rnkR + (1 << i)) >= len)
rnkR += 1 << i;
if(query(a, b - len + 1, rnkL, rnkR)) ans = len, lenl = len + 1;
else lenr = len - 1;
}
printf("%d
", ans);
}
signed main() {
scanf("%d%d", &n, &q);
scanf("%s", s + 1);
SA();
get_height();
build_st();
rt[0] = build(1, n);
rep(i, 1, n) rt[i] = modify(rt[i - 1], 1, n, rnk[i]);
for(; q; -- q) solve();
return 0;
}