后缀数组-学习笔记
i207M
2018-07-17 22:07:00
## 后缀数组
了解一发变量:
SA:排名为i的后缀的位置
rk:位置为i的后缀的排名
tp:第二关键字的排名为i的后缀的位置,还被用作rank的暂存
tax:每个排名对应的后缀数量
后缀数组就是为了求出sa和rk;
有意思的性质:
$rk[sa[i]]=i$还有$sa[rk[i]]=i$;
## 怎么求?
倍增+基数排序;
何为倍增?先计算每个点开始向后$2^k$个字符的排名,两两拼接,算出$2^{k+1}$个字符的排名,直到区分出所有后缀;
我们已经有了用于排序的二元组,如何快速排序?介于每个数字最大不超过N,我们可以使用进阶桶排:基数排序;
(PS:正宗的基数排序时间复杂度为$O(n\log(Maxn))$)
## 注意
基排求前缀和要循环到M;
## 模板题代码与注释详解
```
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cmath>
#include<cstring>
#include<string>
#include<algorithm>
#include<vector>
#include<map>
#include<set>
#include<list>
#include<queue>
#include<stack>
#include<bitset>
#include<deque>
using namespace std;
#define ll long long
#define inf 0x3f3f3f3f
#define ri register int
#define il inline
#define fi first
#define se second
#define mp make_pair
#define pi pair<int,int>
#define mem0(x) memset((x),0,sizeof (x))
#define mem1(x) memset((x),0x3f,sizeof (x))
#define pb push_back
#define gc getchar
template<class T>void in(T &x)
{
x = 0; bool f = 0; char c = gc();
while (c < '0' || c > '9') {if (c == '-') f = 1; c = gc();}
while ('0' <= c && c <= '9') {x = (x << 3) + (x << 1) + (c ^ 48); c = gc();}
if (f) x = -x;
}
#undef gc
#define N 1000010
char s[N];
int n, m;
// 字符串长度,桶的大小
int sa[N], rk[N];
// SA:排名为i的后缀的位置
// rank:位置为i的后缀的排名
int tp[N], tax[N];
// tp:第二关键字的排名为i的后缀的位置,
// 还被用作rank的暂存
// tax:每个排名对应的后缀数量,用作桶
void rsort() { // 基数排序
for (ri i = 1; i <= m; ++i) tax[i] = 0;
for (ri i = 1; i <= n; ++i) ++tax[rk[i]];
// 第i个桶的内容:第一关键字为第i的数量
// 我们枚举每个位置的第一关键字排名(rk[i]),添加到桶中
for (ri i = 1; i <= m; ++i) tax[i] += tax[i - 1];
// 为了计算方便,我们求出前缀和
// 注意这里是循环到M
for (ri i = n; i >= 1; --i) sa[tax[rk[tp[i]]]--] = tp[i];
// 解释一下:因为第一关键字可能对应很多第二关键字
// 我们要在第一关键字相同的情况下排第二关键字
// 为了方便,我们倒序枚举所有的第二关键字,这样它一定是所在桶的最后一名
// 根据我们就求出的前缀和,它就是所有后缀的第tax[rk[tp[i]]]名
// ((第二关键字排名为i的后缀的位置)的桶编号)的前缀和排名
// 因为我们取出了一个元素,所以这个桶的size要-1
// 这样,这个排名的后缀的位置就是tp[i]
}
void ssort() {
for (ri i = 1; i <= n; ++i) rk[i] = s[i], tp[i] = i;
// 初始时,每个字符的排名就是ASCII码
// 第二关键字就随便给个i
rsort();
// 计算出长度为1的SA和rk
for (ri w = 1, p = 0; p < n && w <= n; m = p, w <<= 1) {
// m = p : 改变桶的大小为排名的数量,当数量为N时,完成区分排名
// w是要求出的后缀的长度
p = 0; // p是计数器,记录有多少个后缀的排名不同
for (ri i = n - w + 1; i <= n; ++i) tp[++p] = i;
// 对于起始位置在[n-w+1,n]的后缀,它们没有第二关键字,所以他们的第二关键字最小
for (ri i = 1; i <= n; ++i)
if (sa[i] > w) tp[++p] = sa[i] - w;
// IMPORTANT
// 枚举每一个排名,看看它距离字符串开头是否>w,这样,这个位置就是一个后缀的第二关键字
// 因为我们要在每一个后缀前接上长度为w的后缀,所以tp[++p] = sa[i] - w;
// 注意,编号越小表示这个后缀的起始位置越靠前
// 我们的第二关键字要从小到大,而我们正好是按照从小到大的顺序枚举每一个后缀
rsort(); // 再来一遍
swap(rk, tp); // WARN swap rank & tp
// 我们已经更新了SA,我们还要更新rk,此时tp已经无用,直接备份上一个rk
rk[sa[1]] = p = 1; // 排名第一的后缀直接加入
for (ri i = 2; i <= n; ++i)
rk[sa[i]] = (tp[sa[i]] == tp[sa[i - 1]] && tp[sa[i] + w] == tp[sa[i - 1] + w])
? p : ++p;
// 这里的条件是指:它的第一关键字和第二关键字是否都和前一个相同,如果是,它和上一个并列
// 如果不是,它的排名要+1
// 检查一下p==n,break;(我们已经区分出所有后缀)
}
}
signed main() {
scanf("%s", s + 1);
n = strlen(s + 1);
m = 127; // 初始桶的大小,因为char最大就是127
ssort();
for (ri i = 1; i <= n; ++i) printf("%d ", sa[i]);
return 0;
}
```
## 扩展
$lcp(x,y)$:字符串x与字符串y的最长公共前缀,在这里**指x号后缀与与y号后缀的最长公共前缀**;
$height[i]=lcp$($sa[i],sa[i$ - $1]$),即排名为i的后缀与排名为i−1的后缀的最长公共前缀;
H[i]:height[rak[i]],即i号后缀与它前一名的后缀的最长公共前缀;
重要性质:$H[i] \geqslant H[i - 1] - 1$;
通过这个我们可以快速求出$H[i]$,$k-1$后一位位匹配即可;
求出$height[i]$:
```
void geth() {
int j, k = 0;
for (ri i = 1; i <= n; ++i) {
if (k) --k;
j = sa[rk[i] - 1];
while (s[i + k] == s[j + k]) ++k;
h[rk[i]] = k;
}
}
```
关于LCP的几条性质
显而易见的
$LCP(i,j)=LCP(j,i)$;
$LCP(i,i)=len(sa[i])=n-sa[i]+1$;
**求解的重要性质:**
$LCP(i,k)=min(height[j])$
对于$i+1<=j<=k$;
## 给定字符串 S,求它所有本质不同的子串个数
![](https://cdn.luogu.com.cn/upload/pic/23968.png)
### 区间?
给定一个字符串,每次询问区间$[l,r]$内本质不同的子串个数;
### HDU4622 代码
```
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cmath>
#include<cstring>
#include<string>
#include<algorithm>
#include<vector>
#include<map>
#include<set>
#include<list>
#include<queue>
#include<stack>
#include<bitset>
#include<deque>
using namespace std;
#define ll long long
#define inf 0x3f3f3f3f
#define ri register int
#define il inline
#define fi first
#define se second
#define mp make_pair
#define pi pair<int,int>
#define mem0(x) memset((x),0,sizeof (x))
#define mem1(x) memset((x),0x3f,sizeof (x))
#define pb push_back
#define gc getchar
template<class T>void in(T &x)
{
x = 0; bool f = 0; char c = gc();
while (c < '0' || c > '9') {if (c == '-') f = 1; c = gc();}
while ('0' <= c && c <= '9') {x = (x << 3) + (x << 1) + (c ^ 48); c = gc();}
if (f) x = -x;
}
#undef gc
#define N 2010
char s[N];
int n, m;
// 字符串长度,桶的大小
int sa[N], rk[N];
// SA:排名为i的后缀的位置
// rank:位置为i的后缀的排名
int tp[N], tax[N];
// tp:第二关键字的排名为i的后缀的位置,
// 还被用作rank的暂存
// tax:每个排名对应的后缀数量,用作桶
void rsort() { // 基数排序
for (ri i = 0; i <= m; ++i) tax[i] = 0;
for (ri i = 1; i <= n; ++i) ++tax[rk[i]];
for (ri i = 1; i <= m; ++i) tax[i] += tax[i - 1];
for (ri i = n; i >= 1; --i) sa[tax[rk[tp[i]]]--] = tp[i];
}
void ssort() {
for (ri i = 1; i <= n; ++i) rk[i] = s[i], tp[i] = i;
rsort();
for (ri w = 1, p = 0; p < n && w <= n; m = p, w <<= 1) {
p = 0;
for (ri i = n - w + 1; i <= n; ++i) tp[++p] = i;
for (ri i = 1; i <= n; ++i)
if (sa[i] > w) tp[++p] = sa[i] - w;
rsort();
swap(rk, tp);
rk[sa[1]] = p = 1;
for (ri i = 2; i <= n; ++i)
rk[sa[i]] = (tp[sa[i]] == tp[sa[i - 1]] && tp[sa[i] + w] == tp[sa[i - 1] + w])
? p : ++p;
}
}
int h[N];
void geth() {
int j, k = 0;
for (ri i = 1; i <= n; ++i) {
if (k) --k;
j = sa[rk[i] - 1];
while (s[i + k] == s[j + k]) ++k;
h[rk[i]] = k;
}
}
int st[N][12];
void init() {
// mem1(st);
for (ri i = 1; i <= n; ++i)st[i][0] = h[i];
for (ri j = 1; (1 << j) <= n; ++j) {
for (ri i = 1; i + (1 << j) - 1 <= n; ++i) {
st[i][j] = min(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]);
}
}
}
il int qmin(int l, int r) { // 纯ST表模板,求[l,r]最小值
int k = 0, len = r - l + 1;
while ((1 << (k + 1)) <= len) ++k;
return min(st[l][k], st[r - (1 << k) + 1][k]);
}
il int lcp(int l, int r) { // 利用数组H的性质,求出排名为l的后缀与排名为r的后缀的最长公共前缀
if (l > r) swap(l, r); // 很重要,根据算法,l,r大小不确定
return qmin(l + 1, r); // 排名l,r的后缀的最长公共前缀为min(h[l+1~j])
}
il int query(int l, int r) {
vector<int>pos;
for (ri i = 1; i <= n; ++i)
if (l <= sa[i] && sa[i] <= r) pos.pb(sa[i]); // pos存放在[l,r]之间的后缀
int sum = r - pos[0] + 1, tmp = sum; // sum为答案
// tmp:上一个字符串与之前所有字符串的最长公共前缀长度
for (ri i = 1; i < pos.size(); ++i) {
int k = lcp(rk[pos[i]], rk[pos[i - 1]]), len = r - pos[i] + 1;
// k:当前字符串与上一个字符串的最长公共前缀
tmp = min(tmp, k);
// 如果k<tmp,下一个tmp一定为k
tmp = max(tmp, min(k, r - pos[i - 1] + 1));
// 如果k>tmp,下一个tmp可能会为k,但是不能超过上一个后缀的区间长度,不然不符合规定
sum += len - min(tmp, len);
// 答案加上当前后缀的不同的前缀
}
return sum;
}
int t, q, l, r;
signed main() {
in(t);
while (t--) {
scanf("%s", s + 1);
n = strlen(s + 1);
m = 127;
ssort();
geth(); // GET Height
init(); // init ST
in(q);
while (q--) {
in(l), in(r);
printf("%d\n", query(l, r));
}
}
return 0;
}
```