【做题记录】后缀数组应用
本文中用 \(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\)。
\(\sum\limits_{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}(n\log n)\)。
[AHOI2013] 差异
先不管 lcp。长度为 \(l\) 的后缀出现在 \(T_i\) 的次数是 \(l-1\),出现在 \(T_j\) 的次数是 \(n-l\),那么总和是 \(\sum\limits_{l=1}^nl(l-1+n-l)=(n-1)\sum\limits_{l=1}^nl=\frac{1}{2}n(n-1)(n+1)\)。
接下来要求两两后缀的 lcp。我们知道 \(\operatorname{lcp}(i,j)=\min\limits_{rnk_i<k\le rnk_j}\{he_k\}\),但即使用 st 表也是 \(\operatorname{O}(n^2)\) 的。考虑每个 \(he_i\) 的贡献。用 单调栈 求出 \(L_i,R_i\),分别是 \(i\) 往前数、往后数第一个 \(he\) 值小于 \(he_i\)的,此时 \(\min\limits_{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)+1\ge k\) 即 \(R-L+1\ge k-1\)。如果区间 \([L,R]\) 向左或向右扩展一点,\(\operatorname{lcp}\) 一定不会增大。所以只需要对所有长度为 \(k-1\) 的区间的 \(he\) 最小值求最大值即可,用单调队列 \(\operatorname{O}(n)\),st 表\(\operatorname{O}(n\log n)\)。
Long Long Message
题意:求两字符串的最长公共子串。
用 SA 解决多串问题时,可以考虑把字符串首尾相连。用特殊字符隔开(每个特殊字符互不相同)可以避免 lcp 横跨两个字符串的问题。
假设连接后字符串为 \(S[1..n]\),分隔字符下标为 \(sep\),题目转化成求 \(\max\limits_{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)
题意:求子串长度 \(\times\) 出现次数的最大值。
首先不难想到这个子串的长度一定是 \(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)\times 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\),使得 \(a\le p\le b-len+1\) 且 \(\operatorname{lcp}(p,c)\ge len\)
考虑一个后缀的排名 \(k\)
\(k<rnk_c\) 时,如果 \(k\) 越小,\(\min\limits_{k<i\le rnk_c}\{he_i\}\) 单调不增。
\(k>rnk_c\) 时同理。
那么,使得 \(\operatorname{lcp}(p,c)\ge len\) 的后缀 \(p\) 的 排名 一定是一段区间。可以用 st 表加二分求出这个排名区间。
把下标看成第一维,排名看成第二维,变成了一个二维数点问题。再用一个主席树即可。
时间复杂度 \(O(n\log^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\n", 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;
}