后缀数组模板
*
关于后缀数组的几个性质:
- LCP(i,j)=LCP(j,i)LCP(i, j) = LCP(j, i)LCP(i,j)=LCP(j,i)
- LCP(i,k)=min(LCP(i,j),LCP(j,k))LCP(i, k) = min(LCP(i, j), LCP(j, k))LCP(i,k)=min(LCP(i,j),LCP(j,k)),0≤i≤j≤k<n0 \leq i \leq j \leq k < n0≤i≤j≤k<n
简单证明:
设 LCP(i,j)=p,LCP(j,k)=qLCP(i,j)=p, LCP(j,k)=qLCP(i,j)=p,LCP(j,k)=q,posi=sa[i],posj=sa[j],posk=sa[k]pos_i=sa[i],pos_j=sa[j],pos_k=sa[k]posi=sa[i],posj=sa[j],posk=sa[k]
那么,一定存在 s[posi∼posi+p−1]=s[posj∼posj+p−1]s[pos_i \sim pos_i+p-1]=s[pos_j \sim pos_j+p-1]s[posi∼posi+p−1]=s[posj∼posj+p−1],s[posj∼posj+q−1]=s[posk∼posk+q−1]s[pos_j \sim pos_j+q-1]=s[pos_k \sim pos_k+q-1]s[posj∼posj+q−1]=s[posk∼posk+q−1]。
假设 LCP(i,k)=x<min(p,q)LCP(i,k)=x < min(p,q)LCP(i,k)=x<min(p,q),存在 s[posi+x]≠s[posk+x]s[pos_i+x] \neq s[pos_k+x]s[posi+x]̸=s[posk+x]。
又因为 p,qp,qp,q 均大与 xxx,所以有 s[posi+x]=s[posj+x]s[pos_i+x] = s[pos_j+x]s[posi+x]=s[posj+x] 且 s[posj+x]=s[posk+x]s[pos_j+x]=s[pos_k+x]s[posj+x]=s[posk+x],推出 s[posi+x]=s[posk+x]s[pos_i+x]=s[pos_k+x]s[posi+x]=s[posk+x]。与假设矛盾,所以 LCP(i,k)≥min(p,q)LCP(i,k) \geq min(p,q)LCP(i,k)≥min(p,q)。
同理可证明出 LCP(i,k)≤min(p,q)LCP(i,k) \leq min(p,q)LCP(i,k)≤min(p,q),所以 LCP(i,k)=min(p,q)LCP(i,k) = min(p,q)LCP(i,k)=min(p,q)。 - LCP(i,j)=min{LCP(k,k+1)}LCP(i, j) = min \{ LCP(k, k + 1) \}LCP(i,j)=min{LCP(k,k+1)}, i≤k<ji \leq k < ji≤k<j
可以通过性质 222 推出。 - height[rank[i]]≥height[rank[i−1]]−1height[rank[i]] \geq height[rank[i - 1]] - 1height[rank[i]]≥height[rank[i−1]]−1
可以利用这个性质推出任意两个后缀的 LCPLCPLCP。
ps: LCP(i,j)LCP(i, j)LCP(i,j) 表示 sa[i]sa[i]sa[i] 和 sa[j]sa[j]sa[j] 两个后缀的最长公共前缀,即排名为 iii 和排名为 jjj 的两个后缀的最长公共前缀。height[i]=LCP(sa[i],sa[i−1])height[i]=LCP(sa[i], sa[i-1])height[i]=LCP(sa[i],sa[i−1])。
变量意义解释:
sa[i]sa[i]sa[i]:排名为 iii 的后缀在原字符串的位置
rnk[i]rnk[i]rnk[i]:第 iii 个后缀的排名,与 sasasa 数组意义相反
tmp[i]tmp[i]tmp[i]:辅助数组
height[i]height[i]height[i]:表示 sa[i]sa[i]sa[i] 和 sa[i−1]sa[i-1]sa[i−1] 两个后缀的 LCPLCPLCP
c[]c[]c[]:基数排序用的
nnn:字符串的长度
mmm:每次需要比较的最大值的大小
代码实现:
pss: 任意位置后缀的 LCPLCPLCP 可以使用 STSTST 表来求,代码中只是根据性质 444 计算了任意位置的 LCPLCPLCP。
const int maxn = 1e5 + 5;
string s;
int sa[maxn], rnk[maxn], tmp[maxn], c[maxn], height[maxn];
int n, m;
void build_sa()
{
m = 128; // 初始时是以字符来比较大小的
for(int i = 0; i < m; i++) c[i] = 0;
// 第一次基数排序
for(int i = 0; i < n; i++) c[rnk[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[rnk[i]]] = i;
// 倍增计算
for(int k = 1; k <= n; k <<= 1){
int p = 0;
// 利用sa数组对第二关键词进行排序
// 第i个后缀的第二关键词就是 位置为i+k,长度为k的串
// 这里的tmp数组和sa数组意义一样,代表的是第二关键词
// [n - k, n) 这些位置的后缀没有第二关键词,所以第二关键词为0,此时它们的第二关键词排名是最小的
for(int i = n - k; i < n; i++) tmp[p++] = i;
// [0, n - k) 这些位置的后缀有第二关键词,因此可以直接根据sa数组得到第二关键词排名
// tmp[p++] = sa[i] - k 表示在sa[i]-k位置的后缀的第二关键词的排名为p
for(int i = 0; i < n; i++) if(sa[i] >= k) tmp[p++] = sa[i] - k;
// 利用基数排序对第一关键词进行排序
// 上一次迭代得到的rnk就是当前的第一关键词,rnk[tmp[i]]表示第二关键词排名为i的第一关键词
for(int i = 0; i < m; i++) c[i] = 0;
for(int i = 0; i < n; i++) c[rnk[tmp[i]]]++;
for(int i = 1; i < m; i++) c[i] += c[i - 1];
for(int i = n - 1; i >= 0; i--) sa[--c[rnk[tmp[i]]]] = tmp[i]; // 记住这里不是等于i
// 以下是重新计算rnk数组
swap(rnk, tmp);
rnk[sa[0]] = 0;
p = 1;
// 如果排名为i和i-1的两个串的第一关键词和第二关键词相同,则它们的排名相同;否则,排名增加1
for(int i = 1; i < n; i++)
rnk[sa[i]] = (tmp[sa[i]] == tmp[sa[i - 1]] && tmp[sa[i] + k] == tmp[sa[i - 1] + k]) ? p - 1 : p++;
// 如果没有两个后缀的排名相同,则排序完成
if(p >= n) break;
// 下一次迭代需要比较的最大值
m = p;
}
}
void get_height()
{
for(int i = 0, k = 0; i < n; i++){
if(k) --k; // 性质4在这个体现
if(rnk[i] == 0) continue;
int j = sa[rnk[i] - 1];
while(i + k < n && j + k < n && s[i + k] == s[j + k]) ++k;
height[rnk[i]] = k;
}
}
// 计算位置i和j的两个后缀的lcp
int lcp(int i, int j)
{
if(rnk[i] > rnk[j]) swap(i, j);
int ret = 0;
if(i == j) ret = s.length() - i;
else{
ret = INT_MAX;
for(int k = rnk[i] + 1; k <= rnk[j]; k++)
ret = min(ret, height[k]);
}
return ret;
}