后缀数组(SA)
后缀数组
P3809 【模板】后缀排序
定义:
- 对给定字符串的所有后缀排序后得到的sa、rk数组
sa[i]->排名为i的后缀的位置 rk[i]->位置为i的后缀的排名
容易发现,sa与rk互为逆,如:sa[rk[i]]=i,rk[sa[i]]=i
应用
- 应用主要体现在用后缀数组求lcp与height来维护查找子串、匹配等问题
实现
- 使用的牢柯前辈提供的高级思路,代码极其简略,好写好调
- 主要思想:由于注意到后缀的特殊性,即在依据以i开始的后缀的前len位排好序后,以从i+len+1开始的后缀作为第二关键字继续排序,仍保证正确性且规模缩小(len变长了)。由于对于两个后缀,若前半部分不同,则前半部分小的必定小,否则只需比较后半部分。因此考虑倍增k,一步一步合并。
普通的倍增做法需要记录rk1,rk2作为第一、第二关键字。但牢柯考虑将第一关键字相同的集中处理完,以第二关键字排序,便省去了记录第一关键字。同时,由于没有第一关键字,因此可以很简单地写sort。但复杂度仍然没懂,感觉不太能保证,但事实就是正确的,小柯说不太好讲,知道就行了,复杂度是\(O(nlogn)\) - 省掉了一个计数排序,然后就要统计这一轮的第二关键字的rk来作为下一轮的第一关键字。若两个后缀i,j本身与i+k,j+k相同,即两个后缀前k位相同,那就先视这两个后缀相同,排名不变,若不同,因为sa已经排好序,那后面的rk必定比前面大1。
code:
const int N=1e6+5;
int sa[N],rk[N],rk0[N],n,k=0; //n->s.length
string s;
bool cmp(int x,int y)
{
return rk[x+k]<rk[y+k];
}
void SA() //sa[i]->排名为i的后缀的位置 rk[i]->位置为i的后缀的排名
{
for(int i=1;i<=n;i++) sa[i]=i,rk[i]=s[i];
//初始化排名,rk可以直接等于s[i]是因为第一次倍增只要要求相同字符rk相同即可
sort(sa+1,sa+n+1,cmp);
//处理sa 将sa以其后缀初始位置本身的大小排序,做到要求的"相同第一关键字在sa上连续"
for(k=1;k<=n;k<<=1) //倍增k
{
for(int l=1,r;l<=n;l=r+1)
{
r=l;
while(r<n&&rk[sa[r]]==rk[sa[r+1]]) ++r;
//由于保证了"相同第一关键字在sa上连续",因此直接枚举出此时第一关键字相同的区间
sort(sa+l,sa+r+1,cmp);
//按后缀的后半段即第二关键字排序,处理出新的sa,同时使第二关键字相同的串连续
}
int m=rk0[sa[1]]=1;//赋初值
for(int i=2;i<=n;i++)
rk0[sa[i]]=(rk[sa[i]]==rk[sa[i-1]]&&rk[sa[i]+k]==rk[sa[i-1]+k])?m:++m;
//若第一、第二关键字都相同,则暂时视为同一种串
for(int i=1;i<=n;i++) rk[i]=rk0[i];
if(m==n) return;//若所有rk均不同,则代表已经处理完毕
}
}
P4051 [JSOI2007] 字符加密
- 题意:给定一个字符串,求将其排成一圈所有的读法排序后最后一位组成的序列。\(len\le 1e6\)
- 事实上很好做。考虑将字符串复制在后面,然后求后缀数组。
显然对于后缀排序后的点与按题目中的方式排序相对位置不变,因此直接统计即可。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int sa[N],rk[N],rk0[N],k=0;
bool cmp(int x,int y){return rk[x+k]<rk[y+k];}
void SA(string s,int n)
{
for(int i=1;i<=n;i++) sa[i]=i,rk[i]=s[i];
sort(sa+1,sa+n+1,cmp);
for(k=1;k<=n;k<<=1)
{
for(int l=1,r;l<=n;l=r+1)
{
r=l;while(r<n&&rk[sa[r]]==rk[sa[r+1]]) r++;
sort(sa+l,sa+r+1,cmp);
}
int m=1;rk0[sa[1]]=1;
for(int i=2;i<=n;i++) rk0[sa[i]]=rk[sa[i]]==rk[sa[i-1]]&&rk[sa[i]+k]==rk[sa[i-1]+k]?m:++m;
for(int i=1;i<=n;i++) rk[i]=rk0[i];
if(m==n) continue;
}
}
string t;
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n;string s;cin>>s;s=s+s;n=s.length();s=' '+s;
SA(s,n);
for(int i=1;i<=n;i++) if(sa[i]<=n/2) t+=s[sa[i]+n/2-1];
cout<<t;
return 0;
}
在字符串 \(t\) 中寻找特定子串 \(s\)
- 由于 \(s\) 如果在 \(t\) 中,那么其一定是 \(t\) 的某个后缀的前缀。由于我们已经对 \(t\) 的后缀排序,因此有可二分性。
直接二分 \(O(|s|)\) 地比较,总时间复杂度是 \(O(|s|log_2|t|)\) 的。 - 如果要求 \(s\) 在 \(t\) 中出现的次数,也很好做。
由于出现了 \(s\) 的后缀在后缀数组上一定是连续的,因此还是去二分区间左右端点,时间复杂度不变
P2870 [USACO07DEC] Best Cow Line G
- 题意:从字符串首尾取字符连在一起并最小化字典序
- 考虑类似贪心的做法。如果两边字符不同,那选更小的那一个一定优。如果相同,那就一个一个向里比,比到有不同的为止。
但显然 \(O(n^2)\),考虑有没有方法优化比较的过程。由于后缀数组是对后缀排序,而这里是对串的前缀与后缀大小比较。
因此考虑将原串及其反串连起来。但因为如果这样原串与反串连接可能导致排序混乱,因此之间加一个特殊字符后再做sa。
这样每次对比 \(O(1)\),就可以做到总体时间复杂度 \(O(nlogn)\)(sa本身速度是阈值)。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+7;
int sa[N],rk[N],rk0[N],k=0;string t;
bool cmp(int x,int y){return rk[x+k]<rk[y+k];}
void SA(string s,int n)
{
for(int i=1;i<=n;i++) sa[i]=i,rk[i]=s[i];
sort(sa+1,sa+n+1,cmp);
for(k=1;k<=n;k<<=1)
{
for(int l=1,r;l<=n;l=r+1)
{
r=l;while(r<n&&rk[sa[r]]==rk[sa[r+1]]) ++r;
sort(sa+l,sa+r+1,cmp);
}
rk0[sa[1]]=1;int m=1;
for(int i=2;i<=n;i++) rk0[sa[i]]=rk[sa[i]]==rk[sa[i-1]]&&rk[sa[i]+k]==rk[sa[i-1]+k]?m:++m;
for(int i=1;i<=n;i++) rk[i]=rk0[i];
if(m==n) return;
}
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n,nn;string s;cin>>nn;n=nn;s=' ';string x;for(int i=1;i<=n;i++) cin>>x,s+=x;s+=' ';
for(int i=n;i>=1;i--) s+=s[i];n=s.length()-1;SA(s,n);
int l=1,r=nn+2;
for(int i=1;i<=nn;i++)
{
if(rk[l]<rk[r]) cout<<s[l],l++;else cout<<s[r],r++;
if(!(i%80)) cout<<'\n';
}
return 0;
}
lcp
- \(lcp(i,j)\) 是指两个字符串 \(s_i,s_j\) 的最长公共前缀。这在许多题目中是解题的关键
下面就讲述了如何快速求字符串的两个任意子串的lcp
height数组
定义
- \(height_i\) 表示 \(lcp(sa[i],sa[i-1])\),这里括号里的数值指的是以这个数开头的字符串的后缀,后文也有这种用法。
特别地,\(height_1=0\)
计算
- 首先有一个定理,对于 \(1\le i \le len_s\) 有
\]
结论详见oi wiki(还是太菜了)
- 求法就相对显然了。由于不等式的递减性,只需要 \(O(n)\) 算出来 \(height_{rk_i}\) 的值就可以总复杂度 \(O(n)\) 地求 \(height\) 数组。
当然也可以像oi wiki上一样正着求。
code
for(int i=1,k=0;i<=n;i++)
{
if(k) k--;
while(s[i+k]==s[sa[rk[i]-1]+k]) k++;
heigth[rk[i]]=k;
}
这里代码的逻辑是通过将 \(height_{rk_i}\) 初始化成 \(height_{rk_{i-1}}-1\),再重复去延伸。
结合height数组求lcp
- 还是有一个定理。若求以 \(lcp(i,j)\),有
\]
- 证明看起来非常专业,就不搞了。还是看oi wiki上的感性理解吧
因此在求完 \(height\) 数组后,求 \(lcp\) 就变成了一个RMQ(区间最值)问题。一般解决方法是写一个ST表解决。
P2852 [USACO06DEC] Milk Patterns G
- 题意:给定一串数,求出现次数至少为 \(k\) 次的子串的最大长度。
- 显然在后缀数组拍完序后,对于所有相同子串,由于其一定是某段后缀的前缀,因此这些相同的子串一定在排完序后是连续的。
又因为要求至少出现 \(k\) 次,因此考虑在 \(height\) 数组上用单调队列求 \(height\) 上区间的最小值。
注意,由于 \(height\) 数组是排序后相邻两个字符串的 \(lcp\),因此实际上单调队列维护的区间长度应该是 \(k-1\)(调我两天)
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+7;
int n,kk,s[N],sa[N],rk[N],rk0[N],k=0,q[N],h[N];
bool cmp(int x,int y){return rk[x+k]<rk[y+k];}
void SA()
{
for(int i=1;i<=n;i++) sa[i]=i,rk[i]=s[i];
sort(sa+1,sa+n+1,cmp);
for(k=1;k<=n;k<<=1)
{
for(int l=1,r;l<=n;l=r+1)
{
r=l;
while(r<n&&rk[sa[r]]==rk[sa[r+1]]) ++r;
sort(sa+l,sa+r+1,cmp);
}
rk0[sa[1]]=1;int m=1;
for(int i=2;i<=n;i++) rk0[sa[i]]=rk[sa[i]]==rk[sa[i-1]]&&rk[sa[i]+k]==rk[sa[i-1]+k]?m:++m;
for(int i=1;i<=n;i++) rk[i]=rk0[i];
if(m==n) return;
}
}
void geth()
{
for(int i=1,k=0;i<=n;i++)
{
if(k) k--;
while(i+k<=n&&sa[rk[i]-1]+k<=n&&s[i+k]==s[sa[rk[i]-1]+k]) k++;
h[rk[i]]=k;
}
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>kk;for(int i=1;i<=n;i++) cin>>s[i];
SA();geth();kk--;
int ans=0;
int head=1,tail=0;
for(int i=1;i<=n;i++)
{
while(head<=tail&&q[head]<=i-kk) head++;
while(head<=tail&&h[q[tail]]>h[i]) tail--;
q[++tail]=i;
if(i>=kk) ans=max(ans,h[q[head]]);
}
cout<<ans<<'\n';
return 0;
}
P2408 不同子串个数
- 题意:给定一个字符串,求其本质不同的子串个数。
- 首先,总子串个数显然为 \(\frac{n\times (n+1)}{2}\),考虑如何判掉重复的。
思考方式与前几道题类似,由于所有子串都是后缀的某一前缀,因此相同的子串一定是所有 \(lcp\) 的所有前缀所形成的集合。
注意,这里是所有 \(lcp\) 的前缀所形成的集合,如果有两个相同的子串,其一定是在 SA 数组上是连续的。因此我们可以直接统计不同的数的个数。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+7;
ll n,sa[N],rk[N],rk0[N],k=0,h[N];string s;
bool cmp(int x,int y){return rk[x+k]<rk[y+k];}
void SA()
{
for(int i=1;i<=n;i++) sa[i]=i,rk[i]=s[i];
sort(sa+1,sa+n+1,cmp);
for(k=1;k<=n;k<<=1)
{
for(int l=1,r;l<=n;l=r+1)
{
r=l;while(r<n&&rk[sa[r]]==rk[sa[r+1]]) ++r;
sort(sa+l,sa+r+1,cmp);
}
rk0[sa[1]]=1;int m=1;
for(int i=2;i<=n;i++) rk0[sa[i]]=rk[sa[i]]==rk[sa[i-1]]&&rk[sa[i]+k]==rk[sa[i-1]+k]?m:++m;
for(int i=1;i<=n;i++) rk[i]=rk0[i];if(m==n) return;
}
}
void geth()
{
for(int i=1,k=0;i<=n;i++)
{
if(k) k--;
while(s[i+k]==s[sa[rk[i]-1]+k]) ++k;
h[rk[i]]=k;
}
}
signed main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>s;s=' '+s;
SA();geth();
ll ans=(n+1)*n/2;
for(int i=1;i<=n;i++) ans-=h[i];
cout<<ans<<'\n';
return 0;
}
后缀数组(SA)的更多相关文章
- 后缀数组(SA)总结
后缀数组(SA)总结 这个东西鸽了好久了,今天补一下 概念 后缀数组\(SA\)是什么东西? 它是记录一个字符串每个后缀的字典序的数组 \(sa[i]\):表示排名为\(i\)的后缀是哪一个. \(r ...
- 后缀数组SA学习笔记
什么是后缀数组 后缀数组\(sa[i]\)表示字符串中字典序排名为\(i\)的后缀位置 \(rk[i]\)表示字符串中第\(i\)个后缀的字典序排名 举个例子: ababa a b a b a rk: ...
- 后缀数组SA入门(史上最晦涩难懂的讲解)
参考资料:victorique的博客(有一点锅无伤大雅,记得看评论区),$wzz$ 课件(快去$ftp$%%%),$oi-wiki$以及某个人的帮助(万分感谢!) 首先还是要说一句:我不知道为什么我这 ...
- bzoj3796(后缀数组)(SA四连)
bzoj3796Mushroom追妹纸 题目描述 Mushroom最近看上了一个漂亮妹纸.他选择一种非常经典的手段来表达自己的心意——写情书.考虑到自己的表达能力,Mushroom决定不手写情书.他从 ...
- [笔记]后缀数组SA
参考资料这次是真抄的: 1.后缀数组详解 2.后缀数组-学习笔记 3.后缀数组--处理字符串的有力工具 定义 \(SA\)排名为\(i\)的后缀的位置 \(rk\)位置为\(i\)的后缀的排名 \(t ...
- 【字符串】后缀数组SA
后缀数组 概念 实际上就是将一个字符串的所有后缀按照字典序排序 得到了两个数组 \(sa[i]\) 和 \(rk[i]\),其中 \(sa[i]\) 表示排名为 i 的后缀,\(rk[i]\) 表示后 ...
- 浅谈后缀数组SA
这篇博客不打算讲多么详细,网上关于后缀数组的blog比我讲的好多了,这一篇博客我是为自己加深印象写的. 给你们分享了那么多,容我自私一回吧~ 参考资料:这位dalao的blog 一.关于求Suffix ...
- 后缀数组SA
复杂度:O(nlogn) 注:从0到n-1 const int maxn=1e5; char s[maxn]; int sa[maxn],Rank[maxn],height[maxn],rmq[max ...
- 洛谷2408不同字串个数/SPOJ 694/705 (后缀数组SA)
真是一个三倍经验好题啊. 我们来观察这个题目,首先如果直接整体计算,怕是不太好计算. 首先,我们可以将每个子串都看成一个后缀的的前缀.那我们就可以考虑一个一个后缀来计算了. 为了方便起见,我们选择按照 ...
- 洛谷4248 AHOI2013差异 (后缀数组SA+单调栈)
补博客! 首先我们观察题目中给的那个求\(ans\)的方法,其实前两项没什么用处,直接\(for\)一遍就求得了 for (int i=1;i<=n;i++) ans=ans+i*(n-1); ...
随机推荐
- 20 分钟高效掌握 cursor
本身属于在前端小组的一次小分享,这里做个同步分享. 一.pro 权益说明 目前公司购买了 2 个 pro 月付账号,权益包括无限制 tab 补全与 ai 聊天,但每个月只有 500 个快速请求权益,如 ...
- 奥特曼autMan对接QQ机器人框架go-cqhttp的详细教程
前言 node-onebo用pm2,screen守护都不稳定,node-onebot扫码后ctrl+c,pm2没守护直接掉了,screen只能守护几个小时.因为服务器地区与你常登q的位置不同,时常有密 ...
- Flink - [04] 窗口(Windows)
题记部分 一.Flink中的窗口是什么 (1)一般真实的流都是无界的,怎样处理无界的数据? (2)可以把无限的数据流进行切分,得到有限的数据集进行处理 -- 也就是得到有界流 (3)窗口(Window ...
- 音视频SDK对比|K歌App中的实时合唱功能如何进行技术选型
摘要 在线K歌软件的开发有许多技术难点,需考虑到音频录制和处理.实时音频传输和同步.音频压缩和解压缩.设备兼容性问题等技术难点外,此外,开发者还应关注音乐版权问题,确保开发的应用合规合法. 前言 前面 ...
- SuiGo智能博客系统
一款由Golang+Vue开发的博客类网站,支持大模型对话编写智能博客,同时适配PC和移动端. 功能点说明 系统主要包括 1.博客功能:博客编写也可对话AI模型协助编写.查询.编辑页面.详情页面.分享 ...
- ADO.NET中SQL绑定变量方式总结
最近在项目上遇到几个问题,关于ADO.NET中SQL绑定变量 总结一下,分享给大家. 1. 使用 SqlParameter(推荐方式,防止 SQL 注入) ADO.NET 提供 SqlParamete ...
- go declared and not used
Go语言在代码规范中定义未使用的变量会报"declared and not used"错误 package main import "fmt" func mai ...
- Linux下查询tomcat进程命令
由于查询tomcat进程时将ps -ef|grep tomcat命令记错为ps -f|grep tomcat命令,因此对比两个命令进行区分. ps -f |grep tomcat执行结果: dgztc ...
- 面试题-Java虚拟机
前言 Java虚拟机部分的题目,是我根据Java Guide的面试突击版本V3.0再整理出来的,其中,我选择了一些比较重要的问题,并重新做出相应回答,并添加了一些比较重要的问题,希望对大家起到一定的帮 ...
- Oracle impdp 导入报错 ORA-39083 + ORA-00439
Oracle 11G R2 impdp导入的时候 一直报错: ORA-39083: 对象类型 TABLE:"xxx"."xxx" 创建失败, 出现错误: ORA ...