模板题
给出一个字符串,输出排名为 iii 的后缀的编号,i=1,2,3,...ni=1,2,3,...ni=1,2,3,...n。
一种求法
想当年我字符串题用哈希水遍天下=.=
可以二分 lcplcplcp,然后用哈希判断相不相等,套个 sortsortsort,就能快速对后缀排序。
复杂度是 O(nlog2n)O(nlog^2n)O(nlog2n)。
后缀数组
后缀数组指的是两个数组 saisa_isai 和 rkirk_irki。
rkirk_irki 指的是后缀 [i,n][i,n][i,n] 的排名,saisa_isai 指的是排名为 iii 的后缀 [sai,n][sa_i,n][sai,n]。

性质满足 sa[rk[i]]=rk[sa[i]]=isa[rk[i]]=rk[sa[i]]=isa[rk[i]]=rk[sa[i]]=i。
我们最终要求的就是 saisa_isai 这个序列。
倍增求 sasasa
我们求后缀的排名,可以先比较前 111 位,再比较前 222 位,再比较前 444 位,再比较前 888 位…
也就是每次求出 [i,i+2∗w−1][i,i+2*w-1][i,i+2∗w−1] 的排名,w=1,2,4,8...w=1,2,4,8...w=1,2,4,8...。
求 [i,i+2∗w−1][i,i+2*w-1][i,i+2∗w−1] 的排名,我们可以将它分为 [i,i+w−1][i,i+w-1][i,i+w−1] 和 [i+w,i+2∗w−1][i+w,i+2*w-1][i+w,i+2∗w−1] 两部分。这两部分的排名都已经在上一次循环中求出来了,我们要得到新的排名,其实是让这两部分作为双关键字进行排序。[i,i+w−1][i,i+w-1][i,i+w−1] 的排名为第一关键字,[i+w,i+2∗w−1][i+w,i+2*w-1][i+w,i+2∗w−1] 的排名为第二关键字(如果没有第二关键字认为第二关键字为无穷小或 000),来对所有的 iii 排序。
这个过程可以用一张图片来展现。

于是我们就可以用个 sortsortsort 在 O(nlog2n)O(nlog^2n)O(nlog2n) 内进行后缀排序了。
基数排序
用 sortsortsort 的复杂度和哈希一样,还不如用哈希呢!
注意到这是双关键字,我们可以采用基数排序,先对第二关键字进行排序,再对第一关键字进行排序,这样排序的复杂度是 O(n)O(n)O(n) 的。
总复杂度是 O(nlogn)O(nlogn)O(nlogn) 的。
下面着重讲讲实现细节
记 xi=rkix_i=rk_ixi=rki(这里可以看作第一关键字),yiy_iyi 表示第二关键字排名为 iii 的后缀编号。
核心思想:我们对每一个第一关键字开一个桶,对桶做一遍前缀和,然后按第二关键字的排名倒着将该数对的编号放入第一关键字的桶中,就完成了排序。
代码长这样:
for(i = 1; i <= m; i++) c[i] = 0;//清空桶
for(i = 1; i <= n; i++) c[x[i]]++;//记录每个第一关键字的桶的大小
for(i = 1; i <= m; i++) c[i] += c[i - 1];//对桶做一遍前缀和
for(i = n; i >= 1; i--) sa[c[x[y[i]]]--] = y[i];//按第二关键字的排名倒着将该数对的编号放入第一关键字的桶中
下面举一个栗子。(栗子是盗来的>_<)
假设有这么一些数对,我们要对它按双关键字进行基数排序。
其中第一关键字为:1 3 2 1 4 3 1 21\ 3\ 2\ 1\ 4\ 3\ 1\ 21 3 2 1 4 3 1 2
对应第二关键字为:3 2 1 2 3 3 1 33\ 2\ 1\ 2\ 3\ 3\ 1\ 33 2 1 2 3 3 1 3
所以对应 yyy 数组为:3 7 2 4 1 5 6 83\ 7\ 2\ 4\ 1\ 5\ 6\ 83 7 2 4 1 5 6 8
第一步
清空桶。
第二步
记录桶的大小。
桶的编号:1 2 3 41\ 2\ 3\ 41 2 3 4
桶的大小:3 2 2 13\ 2\ 2\ 13 2 2 1
第三步
对桶做前缀和。
桶的编号:1 2 3 41\ 2\ 3\ 41 2 3 4
桶变成了:3 5 7 83\ 5\ 7\ 83 5 7 8
这时候有个性质。
111 对应的编号为 1−31-31−3
222 对应的编号为 4−54-54−5
333 对应的编号为 6−76-76−7
444 对应的编号为 8−88-88−8
第四步
按第二关键字的排名倒着将该数对的编号放入第一关键字的桶中。
先看第二关键字排名第 888 大的编号,是 888,对应的数对是 (2,3)(2,3)(2,3),第一关键字是 222,于是我们要把这个数对放到 222 的桶中,就放到 555 那里吧(因为它一定是所有以 222 为第一关键字的数对中最小的)。
然后再看排名第 777 大的编号,是 666,对应的数对是 (3,3)(3,3)(3,3),第一关键字是 333,所以放进 333 的桶中,放到 777 那里。
以此类推。
最后的 sasasa 就是:7 4 1 3 8 2 6 57\ 4\ 1\ 3\ 8\ 2\ 6\ 57 4 1 3 8 2 6 5
更新 rkrkrk
得到了新的 sasasa,我们还要求新的 rkrkrk。
就是按着 sasasa 来更新 rkrkrk,如果 saisa_isai 和 sai−1sa_{i-1}sai−1 双关键字都相同,那么 rk[sa[i]]=rk[sa[i−1]]rk[sa[i]]=rk[sa[i-1]]rk[sa[i]]=rk[sa[i−1]]。否则,rk[sa[i]]=rk[sa[i−1]]+1rk[sa[i]]=rk[sa[i-1]]+1rk[sa[i]]=rk[sa[i−1]]+1。
总代码如下
#include <bits/stdc++.h>
#include<ext/pb_ds/hash_policy.hpp>
#include<ext/pb_ds/assoc_container.hpp>
using namespace __gnu_pbds;
using namespace std;
typedef long long LL;
typedef unsigned long long uLL;
struct custom_hash {
static uint64_t splitmix64(uint64_t x) {
x += 0x9e3779b97f4a7c15;
x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
x = (x ^ (x >> 27)) * 0x94d049bb133111eb;
return x ^ (x >> 31);
}
size_t operator()(uint64_t x) const {
static const uint64_t FIXED_RANDOM = chrono::steady_clock::now().time_since_epoch().count();
return splitmix64(x + FIXED_RANDOM);
}
};
LL z = 1;
int read(){
int x, f = 1;
char ch;
while(ch = getchar(), ch < '0' || ch > '9') if(ch == '-') f = -1;
x = ch - '0';
while(ch = getchar(), ch >= '0' && ch <= '9') x = x * 10 + ch - 48;
return x * f;
}
int ksm(int a, int b, int p){
int s = 1;
while(b){
if(b & 1) s = z * s * a % p;
a = z * a * a % p;
b >>= 1;
}
return s;
}
const int N = 2e6 + 5;
int sa[N], x[N], y[N], c[N], num;
char s[N];
int get(int x){
if(x >= '0' && x <= '9') return x - '0' + 1;
if(x >= 'A' && x <= 'Z') return x - 'A' + 11;
if(x >= 'a' && x <= 'z') return x - 'a' + 37;
}
int main(){
int i, j, w, n, m;
scanf("%s", s + 1);
n = strlen(s + 1);
m = 62;//m 是字符集大小
for(i = 1; i <= n; i++) c[x[i] = get(s[i])]++;
for(i = 1; i <= m; i++) c[i] += c[i - 1];
for(i = n; i >= 1; i--) sa[c[x[i]]--] = i;
for(w = 1; w <= n; w <<= 1){
num = 0;
for(i = n - w + 1; i <= n; i++) y[++num] = i;//这部分的第二关键字为无穷小
for(i = 1; i <= n; i++) if(sa[i] > w) y[++num] = sa[i] - w;
//如果 sa[i] 可以作为第二关键字,那么把编号 sa[i]-w 压入 y 中
for(i = 1; i <= m; i++) c[i] = 0;
for(i = 1; i <= n; i++) c[x[i]]++;
for(i = 1; i <= m; i++) c[i] += c[i - 1];
for(i = n; i >= 1; i--) sa[c[x[y[i]]]--] = y[i];
swap(x, y);//由于要更新 x, 又要用到旧的排名,而且 y 已经没用了,就先用 y 来存 x
int p = 0;
for(i = 1; i <= n; i++){
if(y[sa[i]] == y[sa[i - 1]] && y[sa[i] + w] == y[sa[i - 1] + w]) x[sa[i]] = p;
else x[sa[i]] = ++p;
}
if(p == n) break;//如果排好序了就 break
m = p;//一定要更新字符集大小
}
for(i = 1; i <= n; i++) printf("%d ", sa[i]);
return 0;
}
lcp(longgest common prefix)——最长公共前缀
记 lcp(i,j)lcp(i,j)lcp(i,j) 表示 saisa_isai 与 sajsa_jsaj 的最长公共前缀(这个有点反人类),也就是第 iii 小和第 jjj 小的 lcplcplcp。
有两个重要结论:
- lcp(i,j)=min(lcp(i,k),lcp(k,j)) 1≤i≤k≤j≤nlcp(i,j) = min(lcp(i,k),lcp(k,j))\ 1\leq i\leq k\leq j\leq nlcp(i,j)=min(lcp(i,k),lcp(k,j)) 1≤i≤k≤j≤n
- lcp(i,j)=min(lcp(k,k−1)) 1≤i<k≤j≤nlcp(i,j)=min(lcp(k,k-1))\ 1\leq i< k\leq j\leq nlcp(i,j)=min(lcp(k,k−1)) 1≤i<k≤j≤n
证明的话因为我太弱了就略了=.=
记 heighti=lcp(i,i−1)height_i=lcp(i,i-1)heighti=lcp(i,i−1),height1=0height_1=0height1=0
如果我们能求出 heightheightheight 数组,那么要求两个后缀的 lcplcplcp,我们可以用 RMQRMQRMQ 来 O(1)O(1)O(1) 求得。
下面又有一个重要结论(我还是不会证):
height[rk[i]]≥height[rk[i−1]]−1height[rk[i]]\ge height[rk[i-1]]-1height[rk[i]]≥height[rk[i−1]]−1
利用这个,我们可以 O(n)O(n)O(n) 来求得 heightheightheight 数组。
代码长这样:
void geth(){
int i, j, k = 0;
for(i = 1; i <= n; i++) x[sa[i]] = i; //求得 rk 数组
for(i = 1; i <= n; i++){
if(x[i] == 1) continue; //rk[i] = 1时,ht[i] = 0
if(k) k--;
j = sa[x[i] - 1];
while(i + k <= n && j + k <= n && s[i + k] == s[j + k]) k++;
ht[x[i]] = k;
}
}
lcp 的应用
- 求本质不同的子串个数
因为子串一定是后缀的前缀,我们用所有的子串个数,减掉重复的前缀即可。所有子串个数为 n(n+1)2\dfrac{n(n+1)}{2}2n(n+1)。重复的前缀个数为 ∑i=1nheighti\sum\limits_{i=1}^{n}height_ii=1∑nheighti。
其实就是按从小到大枚举后缀,减去这个后缀和前面一个重复的前缀。
这样子做是对的是因为 lcp(i,i−1)lcp(i,i-1)lcp(i,i−1) 一定是 lcp(j,i) (1≤j<i)lcp(j,i)\ (1\leq j<i)lcp(j,i) (1≤j<i) 里面最大的。 - 求至少出现 kkk 次的子串最大长度
最后要求的子串一定是 kkk 个后缀的 lcplcplcp。然后贪心地想,要让这个 lcplcplcp 最大,这 kkk 个后缀一定是连续的(指字典序)。然后我们现在就是求 [i,i+k−1][i,i+k-1][i,i+k−1] 的所有后缀的 lcplcplcp,这个其实就是 lcp(i,i+k−1)lcp(i,i+k-1)lcp(i,i+k−1),又等价于 min(heightj) i<j≤i+k−1min(height_j)\ i<j\leq i+k-1min(heightj) i<j≤i+k−1。用个单调队列就可以了。 - 是否有子串不重叠地出现了至少两次
二分子串长度 ∣s∣|s|∣s∣,然后对每个 iii,求出最大的 jjj,使得 lcp(i,j)≥slcp(i,j)\ge slcp(i,j)≥s。然后用 RMQRMQRMQ 查询 [i,j][i,j][i,j] 的最大下标和最小下标,判断一下即可。
总复杂度还是 O(nlogn)O(nlogn)O(nlogn) 的。
本文详细介绍了后缀数组的构建方法,包括倍增法和基数排序,并讲解了LCP(最长公共前缀)的概念及其应用,如计算本质不同的子串个数和求至少出现k次的子串最大长度等。
561

被折叠的 条评论
为什么被折叠?



