后缀数组 SA 学习笔记
后缀数组 SA 学习笔记
后缀数组处理字符串后缀排名,公共子串类问题十分优秀,可以在部分情况下替代后缀自动机(SAM),本文主要讲解后缀数组的实现过程和部分例题。
算法
定义
后缀:从
后缀数组 SA:SA 是一维数组,
名次数组 rk:rk 是一维数组,
倍增算法
前置知识:基数排序。
使用倍增方法,对字符开始的
当
在求
那么把从
附一张 2009 年集训队论文的图:
这里的
在排序时使用基数排序,排序未完成时
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e6+5;
int n,m=128;
int sa[maxn],rk[maxn],b[maxn],tmp[maxn];
char s[maxn];
int main()
{
scanf("%s",s+1);
n=strlen(s+1);
for(int i=1;i<=n;i++) ++b[rk[i]=s[i]];
for(int i=1;i<=m;i++) b[i]+=b[i-1];
for(int i=n;i;i--) sa[b[rk[i]]--]=i;
for(int i=1;i<=n;i++) tmp[i]=rk[i];
int t=0;
for(int i=1;i<=n;i++)
{
if(tmp[sa[i]]==tmp[sa[i-1]]) rk[sa[i]]=t;
else rk[sa[i]]=++t;
}
m=t;
for(int l=1;l<n;l=l<<1)
{
//此时排序的长度为 2*l
int t=0;
for(int i=n-l+1;i<=n;i++) tmp[++t]=i;//长度不足 l 第二关键字最小
for(int i=1;i<=n;i++) if(sa[i]>l) tmp[++t]=sa[i]-l;
//sa[i] 向后 l 位排第 i 个,按顺序加入 tmp
//由于是 s[sa[i],sa[i]+l] 第二关键字,所以第一关键字为 s[sa[i]-l,sa[i]-1
//此时 tmp 已经按照第二关键字排序
for(int i=1;i<=m;i++) b[i]=0;
for(int i=1;i<=n;i++) b[rk[tmp[i]]]++;//第一关键字排序
for(int i=1;i<=m;i++) b[i]+=b[i-1];
for(int i=n;i;i--) sa[b[rk[tmp[i]]]--]=tmp[i];//基数排序
for(int i=1;i<=n;i++) tmp[i]=rk[i];//读取上次排名,辅助判断关键字相同,辅助修改本次rk
t=0;
for(int i=1;i<=n;i++)
{
if(tmp[sa[i]]==tmp[sa[i-1]]&&tmp[sa[i]+l]==tmp[sa[i-1]+l]) rk[sa[i]]=t;//过程中允许排名相同,判断条件为第一,第二关键字都相同
else rk[sa[i]]=++t;
}
m=t;
}
for(int i=1;i<=n;i++) printf("%d ",sa[i]);
}
当基数排序排出
随机数据情况下,可以大幅度节省时间。
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e6+5;
int n,m=128;
int sa[maxn],rk[maxn],b[maxn],tmp[maxn];
char s[maxn];
int main()
{
scanf("%s",s+1);
n=strlen(s+1);
for(int i=1;i<=n;i++) ++b[rk[i]=s[i]];
for(int i=1;i<=m;i++) b[i]+=b[i-1];
for(int i=n;i;i--) sa[b[rk[i]]--]=i;
for(int i=1;i<=n;i++) tmp[i]=rk[i];
int t=0;
for(int i=1;i<=n;i++)
{
if(tmp[sa[i]]==tmp[sa[i-1]]) rk[sa[i]]=t;
else rk[sa[i]]=++t;
}
m=t;
for(int l=1;l<n;l=l<<1)
{
int t=0;
for(int i=n-l+1;i<=n;i++) tmp[++t]=i;
for(int i=1;i<=n;i++) if(sa[i]>l) tmp[++t]=sa[i]-l;
for(int i=1;i<=m;i++) b[i]=0;
for(int i=1;i<=n;i++) b[rk[tmp[i]]]++;
for(int i=1;i<=m;i++) b[i]+=b[i-1];
for(int i=n;i;i--) sa[b[rk[tmp[i]]]--]=tmp[i];//基数排序
for(int i=1;i<=n;i++) tmp[i]=rk[i];//读取上次排名,修改本次rk
t=0;
for(int i=1;i<=n;i++)
{
if(tmp[sa[i]]==tmp[sa[i-1]]&&tmp[sa[i]+l]==tmp[sa[i-1]+l]) rk[sa[i]]=t;
else rk[sa[i]]=++t;//过程中允许排名相同
}
m=t;
if(m==n) break;
}
for(int i=1;i<=n;i++) printf("%d ",sa[i]);
}
SA-IS
先留个坑
关于后缀数组的应用——height 数组
定义
height 数组:
求 height 数组
如果直接去求 height 数组是
但这里有一个妙不可言的证明,可以把两者联系起来。
排序后,越接近的两个后缀,他们的
其实有一个比这个结论更强的结论,设
人话就是,
感性证明是容易的,下面是写成书面语言的证明:
画一张图。
其中
对于
得证。
上述关于
int k=0;
for(int i=1;i<=n;i++)
{
if(rk[i]==1) continue;
if(k) k--;//k 即为 h[i-1]
int j=sa[rk[i]-1];
while(i+k<=n&&j+k<=n&&s[j+k]==s[i+k]) ++k;
height[rk[i]]=k;
}
height 数组的实际运用
height 数组的实际运用有很多,这里先提出一个运用,后面例题再分析:
求
不妨设
理解一下,有
上图,来自集训队论文 2009 年:
不难证明上述结论,留作习题供读者自己思考。
例题
例1 P4051 JSOI2007 字符加密
长度为
把原字符串复制一次(去除最后一位),后缀排序,然后按顺序输出每个后缀的第
可以这么做的原因是,对排序影响最大的肯定是前
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int n,m;
int sa[maxn],rk[maxn],tmp[maxn],b[maxn];
char s[maxn];
int main()
{
scanf("%s",s+1);
n=strlen(s+1);
for(int i=1;i<n;i++) s[i+n]=s[i];
n=n+n-1;
m=2000;
for(int i=1;i<=n;i++) b[rk[i]=s[i]]++;
for(int i=1;i<=m;i++) b[i]+=b[i-1];
for(int i=n;i;i--) sa[b[rk[i]]--]=i;
for(int i=1;i<=n;i++) tmp[i]=rk[i];
int t=0;
for(int i=1;i<=n;i++)
{
if(tmp[sa[i]]==tmp[sa[i-1]]) rk[sa[i]]=t;
else rk[sa[i]]=++t;
}
m=t;
for(int l=1;l<n;l<<=1)
{
int t=0;
for(int i=n-l+1;i<=n;i++) tmp[++t]=i;
for(int i=1;i<=n;i++) if(sa[i]>l) tmp[++t]=sa[i]-l;
for(int i=1;i<=m;i++) b[i]=0;
for(int i=1;i<=n;i++) b[rk[tmp[i]]]++;
for(int i=1;i<=m;i++) b[i]+=b[i-1];
for(int i=n;i;i--) sa[b[rk[tmp[i]]]--]=tmp[i];
for(int i=1;i<=n;i++) tmp[i]=rk[i];
t=0;
for(int i=1;i<=n;i++)
{
if(tmp[sa[i]]==tmp[sa[i-1]]&&tmp[sa[i]+l]==tmp[sa[i-1]+l]) rk[sa[i]]=t;
else rk[sa[i]]=++t;
}
m=t;
if(m==n) break;
}
for(int i=1;i<=n;i++)
{
if(sa[i]>(n+1)/2) continue;
putchar(s[sa[i]+(n+1)/2-1]);
}
}
例2 P5546 POI2000 公共串
把所有的字符串接在一起,中间用不同的特殊字符分开。
求出 height 数组,然后使用双指针。先使得
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e6+5;
int n,m=128,_;
int sa[maxn],rk[maxn],b[maxn],tmp[maxn],height[maxn],L[10],R[10];
char s[maxn];
int ok;
int vis[10],col[maxn];
void add(int x)
{
if(col[x]==0) return ;
vis[col[x]]++;
if(vis[col[x]]==1) ok++;
}
void del(int x)
{
if(col[x]==0) return ;
vis[col[x]]--;
if(vis[col[x]]==0) ok--;
}
int main()
{
scanf("%d",&_);
for(int i=1;i<=_;i++)
{
L[i]=n+1;
scanf("%s",s+n+1);
n+=strlen(s+n+1);
R[i]=n;
s[++n]=i+'0';
}
for(int i=1;i<=n;i++) b[rk[i]=s[i]]++;
for(int i=1;i<=m;i++) b[i]+=b[i-1];
for(int i=n;i;i--) sa[b[rk[i]]--]=i;
for(int i=1;i<=n;i++) tmp[i]=rk[i];
int t=0;
for(int i=1;i<=n;i++)
{
if(tmp[sa[i]]==tmp[sa[i-1]]) rk[sa[i]]=t;
else rk[sa[i]]=++t;
}
m=t;
for(int l=1;l<n;l=l<<1)
{
int t=0;
for(int i=n-l+1;i<=n;i++) tmp[++t]=i;
for(int i=1;i<=n;i++) if(sa[i]>l) tmp[++t]=sa[i]-l;
for(int i=1;i<=m;i++) b[i]=0;
for(int i=1;i<=n;i++) b[rk[tmp[i]]]++;
for(int i=1;i<=m;i++) b[i]+=b[i-1];
for(int i=n;i;i--) sa[b[rk[tmp[i]]]--]=tmp[i];
for(int i=1;i<=n;i++) tmp[i]=rk[i];
t=0;
for(int i=1;i<=n;i++)
{
if(tmp[sa[i]]==tmp[sa[i-1]]&&tmp[sa[i]+l]==tmp[sa[i-1]+l]) rk[sa[i]]=t;
else rk[sa[i]]=++t;
}
m=t;
if(m==n) break;
}
for(int i=1;i<=n;i++) rk[sa[i]]=i;
int k=0;
for(int i=1;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[j+k]==s[i+k]) ++k;
height[rk[i]]=k;
}
for(int i=1;i<=_;i++)
for(int j=L[i];j<=R[i];j++) col[rk[j]]=i;
deque<int>q;
int l=1,ans=0;
add(1);
for(int r=2;r<=n;r++)
{
while(!q.empty()&&height[q.back()]>=height[r]) q.pop_back();
q.push_back(r);
add(r);
if(ok==_)
{
while(ok==_&&l<r) del(l),l++;
add(l-1),l--;
}
while(!q.empty()&&q.front()<=l) q.pop_front();
if(ok==_) ans=max(ans,height[q.front()]);
}
printf("%d",ans);
}
例3 P2743 USACO5.1 乐曲主题Musical Themes
“转调”可以用差分数组替代,这样就是求这个差分数组的 height,然后二分答案,对 height 进行分组,相邻的大于 mid 的分为一组。
如果有一组最靠前的后缀的起点和最靠后的后缀的起点,之间的距离大于等于 mid。那么这个 mid 是可行的。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+6;
int n,m;
int sa[maxn],tmp[maxn],rk[maxn],b[maxn],s[maxn],height[maxn];
bool check(int mid)
{
int mx=sa[1],mi=sa[1];
for(int i=2;i<=n;i++)
{
if(height[i]<mid) mx=mi=sa[i];
else
{
mi=min(mi,sa[i]),mx=max(mx,sa[i]);
if(mx-mi>mid) return 1;
}
}
return 0;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&s[i]);
for(int i=1;i<n;i++) s[i]=s[i+1]-s[i]+90;
n--;
m=250;
for(int i=1;i<=n;i++) b[rk[i]=s[i]]++;
for(int i=1;i<=m;i++) b[i]+=b[i-1];
for(int i=n;i;i--) sa[b[rk[i]]--]=i;
for(int i=1;i<=n;i++) tmp[i]=rk[i];
int t=0;
for(int i=1;i<=n;i++)
{
if(tmp[sa[i]]==tmp[sa[i-1]]) rk[sa[i]]=t;
else rk[sa[i]]=++t;
}
m=t;
for(int l=1;l<n;l=l<<1)
{
int t=0;
for(int i=n-l+1;i<=n;i++) tmp[++t]=i;
for(int i=1;i<=n;i++) if(sa[i]>l) tmp[++t]=sa[i]-l;
for(int i=1;i<=m;i++) b[i]=0;
for(int i=1;i<=n;i++) b[rk[tmp[i]]]++;
for(int i=1;i<=m;i++) b[i]+=b[i-1];
for(int i=n;i;i--) sa[b[rk[tmp[i]]]--]=tmp[i];
for(int i=1;i<=n;i++) tmp[i]=rk[i];
t=0;
for(int i=1;i<=n;i++)
{
if(tmp[sa[i-1]]==tmp[sa[i]]&&tmp[sa[i-1]+l]==tmp[sa[i]+l]) rk[sa[i]]=t;
else rk[sa[i]]=++t;
}
m=t;
if(m==n) break;
}
for(int i=1;i<=n;i++) rk[sa[i]]=i;
int k=0;
for(int i=1;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;
}
int l=0,r=n,ans=0;
while(l<=r)
{
int mid=(l+r)>>1;
if(check(mid)) ans=mid,l=mid+1;
else r=mid-1;
}
if(ans<4) printf("0");
else printf("%d",ans+1);
}
习题
这两题都运用了后缀数组求
参考资料
2009 年国家集训队论文 《后缀数组——处理字符串的有力工具》——罗穗骞
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现