20230726-后缀数组SA+后缀自动机SAM
20230726
后缀数组
后缀数组 (SA, Suffix Array)
是将字符串的所有后缀排序得到的数组,主要包括两个数组:
\(sa[i]\):将所有后缀按字典序排序后第 \(i\) 小的后缀的开头位置。
\(rk[i]\):表示从第 \(i\) 个字符开始的后缀(我们将它称为后缀 \(i\))的字典序排名
它们满足 \(sa[rk[i]] = rk[sa[i]] = i\)
后缀数组能够帮助我们快速解决许多关于子串、后缀的字符串题目
这里介绍后缀数组的 \(O(n log n)\) 倍增求法。
正常比较字符串的字典序需要先比较第一位,再比较第二位 . . . 。
倍增法,顾名思义就是依次比较所有后缀的前 \(k = 1, 2, 4, . . . 2^c\) 位。
首先按照每个后缀的第一个字符对后缀进行排序
这相当于将这个字符串的每个字符进行排序。
接下来考虑已经对 \(k = n\) 排好了序,如何对 \(k = 2n\) 排序:
将每个子串拆分为前 \(k\) 位与第 \(k + 1 ∼ 2k\) 位,如果能分别求出这两部分的排序,
那么对 \(k = 2n\) 排序相当于对这样一个二元组进行双关键字排序。
前 \(k\) 位的排序已知,对于第 \(k + 1 ∼ 2k\) 位,
注意到后缀 \(i\) 的第 \(k + 1 ∼ 2k\) 位,
就是后缀 \(i + k\) 的前 \(k\) 位,
因此这部分的排序可以通过前 \(k\) 位的排序整体移 \(k\) 位 \(O(n)\) 得到。
最后的双关键字排序由于值域只有 \(n\),可以使用基数排序做到 \(O(n)\)。
这样就可以 \(O(n)\) 得到 \(k = 2n\) 的排序
总复杂度 \(O(n log n)\)
代码还是很优美的
P3809 【模板】后缀排序
题目描述
传送门
读入一个长度为 \(n\) 的由大小写英文字母或数字组成的字符串,
请把这个字符串的所有非空后缀按字典序(用 ASCII 数值比较)从小到大排序,
然后按顺序输出后缀的第一个字符在原串中的位置。
位置编号为 \(1\) 到 \(n\)。
用后缀 \(i\) 表示第 \(i\) 位开始的后缀。
\(n \le 10^6\)
Solution
模板题
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,m,rk[N],sa[N],y[N],c[N];
char s[N];
void getsa(){
m=122;
for(int i=1;i<=n;i++) rk[i]=s[i],c[rk[i]]++;
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=1;i<=n;i++) sa[c[rk[i]]--]=i;
for(int k=1;;k<<=1){
int num=0;
for(int i=n-k+1;i<=n;i++) y[++num]=i;
for(int i=1;i<=n;i++) if(sa[i]>k) y[++num]=sa[i]-k;
for(int i=1;i<=m;i++) c[i]=0;
for(int i=1;i<=n;i++) c[rk[i]]++;
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>=1;i--) sa[c[rk[y[i]]]--]=y[i],y[i]=rk[i];
rk[sa[1]]=num=1;
for(int i=2;i<=n;i++){
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]) rk[sa[i]]=num;
else rk[sa[i]]=++num;
}m=num;
if(num==n) break;
}
}
int main(){
/*2023.7.26 H_W_Y P3809 【模板】后缀排序 后缀数组*/
scanf("%s",s+1);n=strlen(s+1);
getsa();
for(int i=1;i<=n;i++) printf("%d ",sa[i]);
return 0;
}
最长公共前缀-LCP
定义 \(LCP(i, j)\) 表示后缀 \(sa[i]\) 与后缀 \(sa[j]\) 的最长公共前缀。
首先可以只用考虑 \(i \lt j\) 的情况:
- \(LCP(i, j) = LCP(j, i)\)
- \(LCP(i, i) = n − sa[i] + 1\)
LCP Lemma
\(LCP(i, j) = min(LCP(i, k), LCP(k, j))(1 \le i \le k \le j \le n)\)
证明:
令 \(t = min(LCP(i, k), LCP(k, j))\),
那么 \(LCP(i, k) \ge t, LCP(k, j) \ge t\),
后缀 \(sa[i]\) 前 \(t\) 位 \(= sa[k]\) 前 \(t\) 位 \(= sa[j]\) 的前 \(t\) 位,
故 \(LCP(i, j) \ge t\)。
若 \(LCP(i, j) = q \gt t\),
那么 \(i, j\) 的前 \(q\) 个字符相等,
因为 \(t = min(LCP(i, k), LCP(k, j))\),
所以要么 \(sa[i][t + 1]\)(表示后缀 \(sa[i]\) 的第 \(t + 1\)位)\(\lt sa[k][t + 1]\),
要么 \(sa[k][t + 1] \lt sa[j][t + 1]\),
并且 \(sa[i][t + 1] \le sa[k][t + 1] \le sa[j][t + 1]\),
所以 \(sa[i][t + 1] \ne sa[j][t + 1]\),
与假设矛盾,所以 \(LCP(i, j) = t\)
LCP Theorem
\(LCP(i, j) = min(LCP(k − 1, k)) , k \in (i, j]\)
证明:
由 LCP Lemma:
\(LCP(i, j) = min(LCP(i, i + 1), LCP(i + 1, j)\),
然后继续拆下去即可证明。
height数组
令 \(height[i] = LCP(i, i − 1), height[1] = 0\),
那么求出 height
LCP 就是个区间 min 了
令 \(h[i] = height[rk[i]]\),
于是 \(height[i] = h[sa[i]]\),
对 \(h[i]\),有定理:
\(h[i] \ge h[i − 1] − 1\)
证明:
首先我们假设 \(sa[rk[i] − 1] = j, sa[rk[i − 1] − 1] = k\),
于是 \(h[i] = LCP(j, i), h[i − 1] = LCP(k, i − 1)\),
于是我们只需证明 \(LCP(j, i) \ge LCP(k, i − 1) − 1\)
如果后缀 \(k\) 与后缀 \(i − 1\) 首字母不同,显然成立。
如果后缀 \(k\) 与后缀 \(i − 1\) 首字母相同,
那么分别去掉首字母后得到后缀 \(k + 1\) 与后缀 \(i\),
必有 \(rk[k + 1]\) 也 \(\lt rk[i]\),
于是 \(LCP(k + 1, i) = h[i − 1] − 1\),
对于字符串 \(i\),
所有排名比它靠前的字符串中,
与它相似度最高也就是 LCP 最大的一定是紧挨着它的字符串,即 \(j\),
但我们已知 \(k + 1\) 排在 \(i\) 前面并且
\(LCP(k + 1, i) = h[i − 1] − 1\),
那么必然有
\(LCP(j, i) \ge LCP(k + 1, i) = h[i − 1] − 1\),
即 \(h[i] \ge h[i − 1] + 1\)。
void get_height(){
for(int i=1;i<=n;i++) rk[sa[i]]=i;
for(int i=1,k=0;i<=n;i++){
if(rk[i]==1) continue;
if(k) k--;
int j=sa[rk[i]-1];
while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
height[rk[i]]=k;
}
}
SP1811 LCS - Longest Common Substring
题目描述
传送门
求两个串的最长公共子串长度,每个串的长度不超过 250000
Solution
考虑把这两个字符串接起来
注意中间要用一个奇怪的字符隔开
然后再整体跑后缀数组和LCP
这样对于每一个后缀,
我们去找离它最近的其它串的后缀
再求LCP最后ans取max即可
H_W_Y-Coding
#include <bits/stdc++.h>
using namespace std;
const int maxn=5e5+10;
int n,m,rk[maxn],p,sa[maxn],c[maxn],y[maxn],h[maxn],lst[2],ans=0,mn[2];
char s[maxn];
void getsa(){
m=122;
for(int i=1;i<=n;i++) rk[i]=s[i],c[rk[i]]++;
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=1;i<=n;i++) sa[c[rk[i]]--]=i;
for(int k=1;;k<<=1){
int num=0;
for(int i=n-k+1;i<=n;i++) y[++num]=i;
for(int i=1;i<=n;i++) if(sa[i]>k) y[++num]=sa[i]-k;
for(int i=1;i<=m;i++) c[i]=0;
for(int i=1;i<=n;i++) c[rk[i]]++;
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>=1;i--) sa[c[rk[y[i]]]--]=y[i],y[i]=rk[i];
rk[sa[1]]=num=1;
for(int i=2;i<=n;i++){
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]) rk[sa[i]]=num;
else rk[sa[i]]=++num;
}m=num;
if(n==num) return;
}
}
void getheight(){
for(int i=1;i<=n;i++) rk[sa[i]]=i;
for(int i=1,k=0;i<=n;i++){
if(rk[i]==1) continue;
if(k) k--;
int j=sa[rk[i]-1];
while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
h[rk[i]]=k;
}
}
void getans(){
lst[0]=lst[1]=0;mn[0]=mn[1]=0x3f3f3f3f;
for(int i=1;i<=n;i++){
mn[0]=min(mn[0],h[i]);
mn[1]=min(mn[1],h[i]);
if(sa[i]<=p){
lst[0]=i;mn[0]=0x3f3f3f3f;
if(!lst[1]) continue;
ans=max(ans,mn[1]);
}
else if(sa[i]>p+1){
lst[1]=i;mn[1]=0x3f3f3f3f;
if(!lst[0]) continue;
ans=max(ans,mn[0]);
}
}
}
int main(){
/*2023.7.26 H_W_Y SP1811 LCS - Longest Common Substring 后缀数组*/
scanf("%s",s+1);p=strlen(s+1);s[p+1]='$';
scanf("%s",s+p+2);n=strlen(s+1);
getsa();getheight();getans();
printf("%d\n",ans);
return 0;
}
P4051 [JSOI2007] 字符加密-寻找最小的循环移动位置
题目描述
传送门
简单易懂。
Solution
我们考虑把原字符串 \(S\) 复制一份变成 \(SS\)
这样直接进行后缀排序即可
一道板子题……
H_W_Y-Coding
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,sa[N],c[N],cnt=0,rk[N],y[N],m,len;
char s[N];
void getsa(){
m=200;
for(int i=1;i<=n;i++) rk[i]=s[i],c[rk[i]]++;
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=1;i<=n;i++) sa[c[rk[i]]--]=i;
for(int k=1;;k<<=1){
int num=0;
for(int i=n-k+1;i<=n;i++) y[++num]=i;
for(int i=1;i<=n;i++) if(sa[i]>k) y[++num]=sa[i]-k;
for(int i=1;i<=m;i++) c[i]=0;
for(int i=1;i<=n;i++) c[rk[i]]++;
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>=1;i--) sa[c[rk[y[i]]]--]=y[i],y[i]=rk[i];
num=rk[sa[1]]=1;
for(int i=2;i<=n;i++){
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]) rk[sa[i]]=num;
else rk[sa[i]]=++num;
}m=num;
if(num==n) break;
}
}
int main(){
/*2023.7.29 H_W_Y P4051 [JSOI2007] 字符加密 后缀数组*/
scanf("%s",s+1);
n=strlen(s+1);len=n;n*=2;
for(int i=len+1;i<=n;i++) s[i]=s[i-len];
getsa();
for(int i=1;i<=n;i++)
if(sa[i]<=len) putchar(s[sa[i]+len-1]);
printf("\n");
return 0;
}
P2870 [USACO07DEC] Best Cow Line G-从字符串首尾取字符最小化字典序
题目描述
传送门
给你一个字符串,每次从首或尾取一个字符组成字符串,问所有能够组成的字符串中字典序最小的一个。
Solution
发现如果暴力去枚举
我们必须要找到两端往里面走第一个不同的位置才行
而最坏情况是 AAAAAAAAAAA
这样进行暴力时间复杂度就是 \(O(n^2)\)
发现每一次其实我们都是去找的后缀
从后往前也就是把序列翻转过来的后缀
这样就想到了后缀数组
我们把这个序列和翻转的序列放在一起,中间加上一个奇怪的字符
这样一起跑后缀数组
在后面枚举时直接 \(O(1)\) 比较即可
也可以用哈希但是我不会
注意 \(c\) 数组的大小也是 \(N\)
H_W_Y-Coding
#include <bits/stdc++.h>
using namespace std;
const int N=2e6+5;
int n,rk[N],sa[N],c[N],y[N],len=0,l,r,m,cnt=0;
char s[N];
void getsa(){
m=200;
for(int i=1;i<=n;i++) rk[i]=s[i],c[rk[i]]++;
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=1;i<=n;i++) sa[c[rk[i]]--]=i;
for(int k=1;;k<<=1){
int num=0;
for(int i=n-k+1;i<=n;i++) y[++num]=i;
for(int i=1;i<=n;i++) if(sa[i]>k) y[++num]=sa[i]-k;
for(int i=1;i<=m;i++) c[i]=0;
for(int i=1;i<=n;i++) c[rk[i]]++;
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>=1;i--) sa[c[rk[y[i]]]--]=y[i],y[i]=rk[i];
rk[sa[1]]=num=1;
for(int i=2;i<=n;i++){
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]) rk[sa[i]]=num;
else rk[sa[i]]=++num;
}m=num;
if(num==n) break;
}
for(int i=1;i<=n;i++) rk[sa[i]]=i;
}
int main(){
/*2023.7.29 H_W_Y P2870 [USACO07DEC] Best Cow Line G 后缀数组*/
scanf("%d",&n);
for(int i=1;i<=n;i++){
s[i]=getchar();
while(!(s[i]>='A'&&s[i]<='Z')) s[i]=getchar();
s[2*n+2-i]=s[i];
}
s[n+1]='$';len=n;n=2*n+1;
getsa();l=1,r=len;
while(l<=r){
printf("%c",rk[l]<rk[n+1-r]?s[l++]:s[r--]);
if((++cnt)%80==0) puts("");
}
return 0;
}
P2852 [USACO06DEC] Milk Patterns G-出现至少 k 次的子串的最大长度
题目描述
传送门
出现至少 k 次的子串的最大长度
Solution
思路非常简单
就直接求一个 height 就可以了
把连续 \(k-1\) 个 \(h[i]\) 取 min 答案再去 max
注意 \(h[i]\) 的下标 \(i\) 是排名
函数不要写错了……
H_W_Y-Coding
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int c[N<<5],n,m,a[N],rk[N],sa[N],y[N],ans=0,h[N],q[N],head,tail,p;
void getsa(){
m=1e6;
for(int i=1;i<=n;i++) rk[i]=a[i],c[a[i]]++;
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=1;i<=n;i++) sa[c[rk[i]]--]=i;
for(int k=1;;k<<=1){
int num=0;
for(int i=n-k+1;i<=n;i++) y[++num]=i;
for(int i=1;i<=n;i++) if(sa[i]>k) y[++num]=sa[i]-k;
for(int i=1;i<=m;i++) c[i]=0;
for(int i=1;i<=n;i++) c[rk[i]]++;
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>=1;i--) sa[c[rk[y[i]]]--]=y[i],y[i]=rk[i];
num=rk[sa[1]]=1;
for(int i=2;i<=n;i++){
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]) rk[sa[i]]=num;
else rk[sa[i]]=++num;
}m=num;
if(num==n) return;
}
}
void getheight(){
for(int i=1;i<=n;i++) rk[sa[i]]=i;
for(int i=1,k=0;i<=n;i++){
if(rk[i]==1) continue;
if(k) k--;
int j=sa[rk[i]-1];
while(i+k<=n&&j+k<=n&&a[i+k]==a[j+k]) k++;
h[rk[i]]=k;
}
}
int main(){
/*2023.7.29 H_W_Y P2852 [USACO06DEC] Milk Patterns G 后缀数组*/
scanf("%d%d",&n,&p);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
getsa();getheight();
head=1;tail=0;
for(int i=2;i<=p;i++){
while(head<=tail&&h[i]<h[q[tail]]) tail--;
q[++tail]=i;
}
ans=h[q[head]];
for(int i=p+1;i<=n;i++){
while(head<=tail&&q[head]<=i-p+1) head++;
while(head<=tail&&h[i]<h[q[tail]]) tail--;
q[++tail]=i;
ans=max(ans,h[q[head]]);
}
printf("%d\n",ans);
return 0;
}