SA
这应该是第一节能够课上听懂的知识了
算法原理
SA 算法,著名的后缀数组
以下只讨论
的倍增构造
目标:求出
ababa --> 53142
算法核心:倍增法。
我们考虑先求出仅考虑
注意到
因此我们考虑一个很 naive 的算法。
对于
然后排序后重新标记
这个算法复杂度是 sort
。
不难发现我们只需一个双关键字桶排序即可优化到
双关键字桶排序。
我们先对第一关键字建立桶。记录
为第一关键字 的数的总数,这个可以先计数再前缀和。 然后将所有数字按照第二关键字排序,倒序加入即可。
void build(int n,int cnt){ for(int i=1;i<=cnt;i++)c[i]=0; for(int i=1;i<=n;i++)c[a[i]]++,tmp[i]=a[b[i]];//tmp[i]减小缓存,很重要 for(int i=1;i<=cnt;i++)c[i]+=c[i-1]; } void sa_sort(int n){ for(int i=n;i;--i){//b_i:第二关键字排名为i的数的位置(也即g(i+2^k,k)的排名),tmp[i]:b_i所对应的数所对应的桶编号 sa[c[tmp[i]]]=b[i];//sa[i]:排名为i的位置 c[tmp[i]]--; } }
然后我们考虑建立 SA。
void build_sa(char s[],int n){
for(int i=1;i<=n;i++)a[i]=s[i],b[i]=i;
int siz=512;build(n,siz);sa_sort(n);//第一轮排序初始化
for(int len=1;len<n;len<<=1){//len=2^k
int pos=0;
for(int i=n-len+1;i<=n;i++)b[++pos]=i;//标记特殊的,也即g(i+2^k,k)为无穷小
for(int i=1;i<=n;i++)if(sa[i]>len){
b[++pos]=sa[i]-len;
}//对于剩下的数,已知的数进行排序。
build(n,siz);
sa_sort(n);
for(int i=1;i<=n;i++)d[i]=a[i];//copy,重标号第一关键字
a[sa[1]]=1;pos=1;
for(int i=2;i<=n;i++){
if(d[sa[i]]!=d[sa[i-1]]||d[sa[i]+len]!=d[sa[i-1]+len])++pos;
a[sa[i]]=pos;
}
siz=pos;
if(len!=1&&pos==n)break;//剪枝,相当重要,已经排好
}
}
设
HEIGHT
借助 SA 数组,我们可以求出一个数组
有引理:
void build_h(int n){
for(int i=1;i<=n;i++){
h[rk[i]]=max(0,h[rk[i-1]]-1);
while(i+h[rk[i]]<=n&&sa[rk[i]-1]+h[rk[i]]<=n&&s[sa[rk[i]-1]+h[rk[i]]]==s[i+h[rk[i]]])++h[rk[i]];
}
}
复杂度显然线性。
性质:
所以可以通过 st 快速求两个后缀的
void get_lcp(int lcp[21][N]){
memset(h,0,sizeof h);memset(rk,0,sizeof rk);memset(sa,0,sizeof sa);memset(a,0,sizeof a);memset(b,0,sizeof b);
memset(d,0,sizeof d);
build_sa(s,n);build_h(n);
for(int i=1;i<=n;i++)lcp[0][i]=h[i];
for(int j=1;(1<<j)<=n;j++){
for(int i=1;i+(1<<j)-1<=n;i++)lcp[j][i]=min(lcp[j-1][i],lcp[j-1][i+(1<<j-1)]);
}
}
int getlcp(int l,int r){
l=rklcp[l],r=rklcp[r];//important
if(l>r)swap(l,r);++l;
int k=lg[r-l+1];
return min(lcp[k][l],lcp[k][r-(1<<k)+1]);
}
基本应用
比较串 。
先求解
。如果长度大于等于两个串的长度的较小值,则直接比较长度。 否则等价于找到了第一个失配位置。直接比较
复杂度
单次。
字符串匹配
在串
中找到所有串 的出现位置。多次操作。 直接后缀数组上二分,暴力比较即可。复杂度
二分第一个匹配位置和第一个失配位置即可。
本质不同子串数量
。这是一个去重操作。
最小表示法
直接把串复制两倍,后缀排序,找到第一个
即可。
出现 次子串最大长度
本问题形式化描述是求出最大的
,满足存在 个 ,使得 。 问题答案显然是
滑动窗口即可。
最长公共子串
求串
的最长公共子串。 建立串
,求其 。 令
,也即 的起步位置分属 。
最长回文子串
令
即可。
单调栈求出
满足:
。注意等号。
即为答案。证明显然。 本质上是求
的答案。全部都是
形如 循环串处理
基本策略
下面我们简记
注意到如果存在一个串
所以如果我们枚举
那么如果存在一个串
则必然有
因此存在一个串
求解 为以 为开头的 串个数。
我们可以得到影响区间,令
由此,差分计算即可。
求解重复次数最多连续重复子串
本质上还是上面那个问题,最多出现次数就是
综合应用
结合并查集
一个最基本的应用是对于所有的
这个可以使用后缀数组进行操作。
最naive的想法是把后缀数组进行分段,保留若干极长连续段
答案显然是
这里我们完全是对
而我们需要对于每个
利用并查集倒序
int find(int x){
return x==f[x]?x:f[x]=find(f[x]);
}
int get(int a){
int len=rpos[a]-lpos[a]+1;
return len*(len-1)/2;
}
void merge(int a,int b){
a=find(a),b=find(b);
if(a==b)return ;
nowcnt-=get(a)+get(b);
lpos[a]=min(lpos[a],lpos[b]);
rpos[a]=max(rpos[a],rpos[b]);
f[b]=a;nowcnt+=get(a);
}
//solve 函数中
for(int i=1;i<=n;i++)f[i]=i,lpos[i]=rpos[i]=i;
for(int i=2;i<=n;i++)posval[h[i]].push_back(i);
for(int p=n-1;p>=0;--p){
for(auto cut:posval[p]){
merge(cut,cut-1);
}
anscnt[p]=nowcnt;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
2023-02-02 树链剖分习题集
2023-02-02 树链剖分入门